二叉樹(BST)中序遍歷的三種方法

正在學習的黃老師發表於2020-10-23

介紹二叉樹中序遍歷的三種方法,分別是:遞迴(隱式的維護了一個棧)、基於棧的迭代、Morris中序遍歷

首先:什麼是二叉樹中序遍歷:

按照訪問左子樹——根節點——右子樹的方式遍歷這棵樹,而在訪問左子樹或者右子樹的時候我們按照同樣的方式遍歷,直到遍歷完整棵樹。

1.使用遞迴實現中序遍歷:

二叉樹的中序遍歷的整個遍歷過程本身就具有遞迴的性質,可以直接用遞迴函式來模擬這一過程。
定義 inorder(root) 表示當前遍歷到root節點的答案,那麼按照定義,我們只要遞迴呼叫 inorder(root.left) 來遍歷root節點的左子樹,然後將 left root節點的值加入答案,再遞迴呼叫inorder(root.right) 來遍歷 root 節點的右子樹即可,遞迴終止的條件為碰到空節點。

public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        inorder(root,list);
        return list;
    }
    private void inorder(TreeNode root,List<Integer> list){
        //this is the end statement
        if(root==null){
            return;
        }
        inorder(root.left);
        list.add(root.val);
        inorder(root.right);
    }

複雜度分析

時間複雜度:O(n), n 為二叉樹節點的個數。因為遞迴的遍歷中每個節點只會被訪問一次。
空間複雜度:O(n)。在最壞情況下,二叉樹退化為連結串列,此時棧的深度為二叉樹的長度n。

2.使用基於棧的迭代實現中序遍歷

其實在第一種方法裡,遞迴的模擬中序遍歷會隱式的維護一個棧,那如果直接把這個棧顯示的模擬出來,即可使用迭代的方法
這裡使用雙端佇列Deque,目的是想在列表頭和列表尾都實現增添和刪除操作

public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        Deque<TreeNode> deque = new LinkedList<>();
        while (root!=null||!deque.isEmpty()){//這兩者都不成立的時候,直接返回,因為此時root跟為空
            //build a stack contains of all left element,for each element,next will deal with
            //their right element
            while (root!=null){
                deque.add(root);
                root=root.left;
            }
            root = deque.poll();//pull the leftest element
            list.add(root.val);//add it's value into ans list
            root=root.right;//search for it's right node

        }
        return list;
    }

複雜度分析

時間複雜度:O(n),同上,每個節點只會遍歷一次
空間複雜度:O(n)。同上,在最壞情況下,二叉樹退化為連結串列,此時棧的深度為二叉樹的長度n。

3。使用Morris遍歷演算法實現中序遍歷

Morris 遍歷演算法是另一種遍歷二叉樹的方法,它能將非遞迴的中序遍歷空間複雜度降為 O(1)O(1)。

Morris 遍歷演算法整體步驟如下(假設當前遍歷到的節點為 xx):

1.如果 x 無左孩子,先將 x 的值加入答案陣列,再訪問 x 的右孩子,即x=x.right。
2.如果 x 有左孩子,則找到 x 左子樹上最右的節點(即左子樹中序遍歷的最後一個節點,x 在中序遍歷中的前驅節點),我們記為 predecessor。根據 predecessor 的右孩子是否為空,進行如下操作。
如果 predecessor 的右孩子為空,則將其右孩子指向 x,然後訪問 x 的左孩子,x=x.left。
如果 predecessor 的右孩子不為空,則此時其右孩子指向 x,說明我們已經遍歷完 x 的左子樹,我們將 predecessor 的右孩子置空,將 x 的值加入答案陣列,然後訪問 x 的右孩子,x=x.right。
重複上述操作,直至訪問完整棵樹。

本質上:假設當前遍歷到的節點為 x,將 x 的左子樹中最右邊的節點的右孩子指向 x,這樣在左子樹遍歷完成後我們通過這個指向走回了 x,且能通過這個指向知曉我們已經遍歷完成了左子樹,而不用再通過棧來維護,省去了棧的空間複雜度。

public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        TreeNode predecessor = null;
        while(root!=null){
            // predecessor 節點就是當前 root 節點向左走一步,然後一直向右走至無法走為止
            if(root.left!=null){
                predecessor=root.left;
                while(predecessor.right!=null&&predecessor.right!=root){
                    predecessor=predecessor.right;
                }
                //如果Predecessor右邊節點為空,那就把它指向根
                if(predecessor.right==null){
                    predecessor.right = root;
                    root = root.left;
                }
                //如果不為空,那就說明這個左樹其實已經指向root了,已經遍歷完 xx 的左子樹
                else {
                    list.add(root.val);
                    predecessor.right =null;
                    root=root.right;
                }

            }
            //不然左為空,就直接把當前加入答案,然後檢視右樹
            else {
                list.add(root.val);
                root=root.right;
                }
            }
        return list;
        }

複雜度分析

時間複雜度:O(n),其中 n 為二叉搜尋樹的節點個數。Morris 遍歷中每個節點會被訪問兩次,因此總時間複雜度為 O(n)。
空間複雜度:O(1)。因為並不需要維護一個深度為n的棧(在極壞情況下)

參考連結:https://leetcode-cn.com/problems/binary-tree-inorder-traversal/solution/er-cha-shu-de-zhong-xu-bian-li-by-leetcode-solutio/

相關文章