1. 程式人生 > 實用技巧 >二叉樹中序遍歷的三種方式

二叉樹中序遍歷的三種方式

leetcode(94 Binary Tree Inorder Traversal) 題目:

Given a binary tree, return the inorder traversal of its nodes' values.

Example:

Input:
   [1,null,2,3]
   1
    \
     2
    /
   3
Output:   [1,3,2]

一棵樹的中序遍歷分為三步驟:

  • 先遍歷其左子樹
  • 再遍歷本身節點
  • 最後遍歷其右子樹
以下圖為例,中序遍歷的結果為 [4,2,5,1,6,3,7]

先定義樹的結構:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */

1.遞迴方式:

public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        helper(root,result);
        return result;
    }
    public void helper(TreeNode root,List<Integer> result){
        if(root != null){
            if(root.left != null)
                helper(root.left,result);
            result.add(root.val);
            if(root.right !=null)
                helper(root.right,result);
        }
    }

2.迭代方式:

 public List<Integer> inorderTraversal(TreeNode root) {
        if(root == null)
            return new ArrayList<Integer>();
        List<Integer> result = new ArrayList<>();
        Stack<TreeNode> stack = new Stack();
        TreeNode temp = root;
        while(temp !=null || !stack.isEmpty()){
            while(temp != null){
                stack.push(temp);
                temp =temp.left;
            }
            temp = stack.pop();
            result.add(temp.val);
            temp =temp.right;
        }
        return result;
    }

3.莫里斯遍歷方式:

 public List<Integer> inorderTraversal(TreeNode root) {
        if(root == null)
            return new ArrayList<>();
        TreeNode cur = root;   //當前節點的位置
        List<Integer> res = new ArrayList<>();
        while(cur != null){
            if(cur.left ==null){
                res.add(cur.val);
                cur = cur.right;
            }else{
                TreeNode pre = cur.left;
                while(pre.right != null && pre.right !=cur){  //尋找左子樹的最右節點
                    pre = pre.right;
                }
                if(pre.right == null){  //返回父結點
                    pre.right =cur;
                    cur = cur.left;
                }else{                  // 已遍歷過,需要斷開連線
                    pre.right = null;
                    res.add(cur.val);   //中序遍歷存放當前節點
                    cur = cur.right;
                }
            }
        }
        return res;
    }

前兩中方式程式碼一目瞭然,後一種方式需要稍稍闡述下原理與步驟:

原理:

​ 遞迴或者棧的空間複雜度都是O(n),假設我們需要O(1)空間複雜度的遍歷,是否有一種方式可以實現,答案是可以的。我們可以使用線索二叉樹(threaded binary tree)的概念,利用葉子節點的空指標尋找其前序後繼節點,左指標指向前驅節點,右指標指向後繼節點,這樣做到了O(n)時間複雜度,O(1)空間複雜度的遍歷。

如上圖所示,利用葉子節點的空指標指向其前序後繼節點(紅色標識),假如我們想中序遍歷,只需要先尋找出第一個左子樹為null的節點,然後按照線索二叉樹的線索進行判斷是後繼節點還是右子樹,若是後繼節點,則直接遍歷(放入res中) 然後跳轉到其右子樹:

public List<Integer> threadedTreeListByInOrder() {
        HeroNode node = root;
     	List<Integer> res = new ArrayList<>();
        while (node != null) {
            while (node.getLeftType() == 0) {
                node = node.left;
            }
            res.add(node.val);
            while (node.getRightType() == 1) {
                node = node.right;
                res.add(node.val);
            }
            node = node.right;
        }
    	return res; 
    }

​ 但這裡有一個限制(你在程式碼裡也注意到了),就是在定義樹結構的時候,需要宣告兩個線索,用於判斷其左指標是前驅節點還是左子樹:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     int leftType;   //判斷左指標的類別
 *     int rightType;  //判斷右指標的類別
 *     TreeNode(int x) { val = x; }
 * }
 */

​ 這樣本質上,就改變了樹形結構的定義。那有沒有一種既用了線索二叉樹的原理,又不改變其定義呢?答案依然是有的,就是本文第三種遍歷方式:Morris Traversal

Morris Traversal:通過動態建立線索的方法進行遍歷,遍歷某個節點完後,將該節點相關聯的線索(姑且這麼稱呼吧)進行刪除,遍歷完後,不改變樹的結構和原本的資料分佈

實現原則分為2大步:首先記當前節點為cur

  • 1:cur的左子節點為null,遍歷該節點,當前節點cur 指向其右子節點

  • 2:cur的左子節點不為null,設precur的左子節點,pre為一個臨時節點 遍歷尋找pre下的最右側葉節點,並複製給pre [pre =pre.right]

    • 2.1:pre的右子節點 為null, 將當前節點cur賦值給pre的右子節點[pre.right=cur]cur 移到自身的的左子節點[cur = cur.left]
    • 2.2:pre的右子節點為cur,表示precur已經連線了,現在需要遍歷然後結束該連線,所以,錄入cur節點[res.add(cur.val)],並將pre的右子節點置空[pre.right=null],之後cur移到自身的右子節點[cur =cur.right]

    重複上述的2大步直到節點cur為空

    如果前面的原則你有點迷糊,不要緊,我們以上面的例子做個全過程,幫助理解下 ,其中res 存放遍歷後的節點 :

morris遍歷過程:
  • (1):首先cur 從頭節點 1 開始,按照前面morris原則的第二步,它存在左子節點,先搜尋左子節點的最右側節點並賦值給pre,該節點為 5 ,再根據最右側葉節點的值為null,所以5 的右子節點指向1cur移動到自身的左子節點 2 res=[]

  • (2):2 有左子節點,且2的左子節點4 按照morris原則第二步,先搜尋左子節點的最右側節點,根據最右側葉節點的值為null,按照2.1原則,**所以將 4 的右子節點指標指向 2cur移到自身的左子節點 4 res=[]

  • (3):4沒有左子節點,按照morris原則第一步,遍歷該節點並將cur指向該節點的右子節點 (在上一步中,我們已經將4的右指標指向了2,所以我們可以直接進行跳轉至4的右子節點2) res=[4]

  • (4):cur此時回到了2, 2有左子節點,按照morris原則的第二步,搜尋左子節點的最右側節點,發現在搜尋過程中4的右子節點指向了cur,按照前面的2.2原則,4的右指標指向null,遍歷當前節點,同時cur指向其右子節點 5 res=[4,2]

  • (5):5不存在右子節點,按照morris原則的第一步,遍歷該節點並將cur指向該節點的右子節點(根據流程的第一步,我們已經將5節點的右指標指向了1,所以cur跳轉至1) res=[4,2,5]

  • (6):cur此時回到了1,按照morris原則的第二步,搜尋左子節點的最右側節點,發現在搜尋過程中5的右子節點指向了cur,按照前面的2.2原則,5的右指標指向null,遍歷當前節點,同時cur指向其右子節點 3(這一步與流程的第4步一樣) res=[4,2,5,1]

  • (7):3有左子節點7,按照morris原則第二步,先搜尋左子節點的最右側節點,根據最右側葉節點的值為null,按照2.1原則所以將 6 的右子節點指標指向 3cur移到自身的左子節點 6 res=[4,2,5,1]

  • (8):6沒有左子節點,按照morris原則的第一步,遍歷該節點並將cur指向該節點的右子節點res=[4,2,5,1,6]

  • (9):cur此時回到了3,有左子節點6,依然是搜尋左子節點下的最右側節點,搜尋過程中發現右側節點指向cur本身,按照morris原則的2.26的右指標指向null,遍歷當前節點,同時cur指向其右子節點 7 res=[4,2,5,1,6,3]

  • (10) :7沒有左子節點,直接遍歷並將cur指向該節點的右子節點 ,最後cur指向了null,此時整個中序遍歷完成。 res=[4,2,5,1,6,3,7]

​ 空間複雜度:O(1),比較好推斷

​ 時間複雜度:O(n)。可能會疑問以下程式碼跟樹的高度有關:

 while(pre.right != null && pre.right !=cur)  //尋找左子樹的最右節點
                    pre = pre.right;

但事實上,尋找所有節點的前驅節點只需要O(n)時間。以上面的流程為例,尋找前驅節點中所有的節點最多被訪問了兩遍!加上自身的遍歷,總共最多方法3遍。所以,樹的每個節點最多方法3遍,時間複雜度為O(n)。

參考:

Morris Traversal方法遍歷二叉樹(非遞迴,不用棧,O(1)空間)

神級遍歷——morris

94. Binary Tree Inorder Traversal