1. 程式人生 > 其它 >二叉樹的深度優先遍歷之先序中序後序遞迴非遞迴Morris遍歷全解

二叉樹的深度優先遍歷之先序中序後序遞迴非遞迴Morris遍歷全解

遞迴版本

二叉樹的深度優先遍歷,包括:
1.前序遍歷
2.中序遍歷
3.後序遍歷

他們是如何定義的呢?
見程式碼:

public void traverse(TreeNode root) {
    if (root == null) {
        return;
    }
    // 先序,preOrder,第一次到達該節點處
    res.get(0).add(root.val);
    traverse(root.left);

    // 中序,inOrder第二次到達該節點處
    res.get(1).add(root.val);
    traverse(root.right);

    // 後序,postOrder第三次到達該節點處
    res.get(2).add(root.val);
}

如程式碼所示:

  1. 先序遍歷是剛走到該節點便獲取該節點的值,然後遞迴遍歷左子樹、右子樹(中左右);
  2. 中序遍歷是遞迴遍歷完左子樹,然後獲取該節點的值,最後再遞迴遍歷右子樹(左中右);
  3. 後序遍歷是遞迴遍歷完左子樹,再遞迴遍歷右子樹,最後獲取該節點的值(左右中);

注:上述程式碼中res的宣告為:List<List<Integer>> res;

如圖所示:

圖中描述了遞迴在樹的各個節點之間的抽象過程:
圖形說明:

  1. 箭頭指向表示遞迴函式訪問到該節點;
  2. N表示null節點,指向null節點的指標,表示遞迴函式進入到null節點的判斷處。

結合遞迴函式看該圖就很簡單了:

  1. 先序遍歷,就是第一個箭頭指向時列印的地方(中左右,從上往下,剛進入到該節點就獲取該節點的值)
  2. 中序遍歷,就是第二個箭頭指向時列印的地方(左中右,遍歷左邊節點返回,然後再獲取該節點的值,然後訪問右節點)
  3. 後序遍歷,就是第三個箭頭指向時列印的地方(左右中,遍歷完成左、右兩邊節點後返回,最後再獲取該節點的值)

非遞迴版本遍歷的統一模板

前邊圖形描述了遞迴版本的過程,非遞迴版本即模擬該過程即可:

詳見程式碼:

public void nonRecursiveAllInOne(TreeNode root) {
    if (root == null) {
        return;
    }
    TreeNode cur = root;
    // 後序遍歷,上一次訪問的節點
    TreeNode pre = null;
    Deque<TreeNode> stack = new ArrayDeque<>();
    while (!stack.isEmpty() || cur != null) {
        // 一直往左邊走
        while (cur != null) {
            // 先序遍歷,剛訪問該節點就獲取該節點的值
            res.get(0).add(cur.val);
            stack.push(cur);
            cur = cur.left;
        }
        // 一直往左邊走後,cur最後會為空
        // 此時獲取壓入棧中的元素(沒彈出)
        cur = stack.peek();
        // 如果當前節點的右節點是前一次訪問的節點,那麼是第三次回來了,就是後序遍歷
        // 如果當前節點的右節點是空,因為我們程式碼一直往左邊走,所以右邊的null節點是沒法訪問到的(而且右邊的null節點也沒必要訪問),此時,中序遍歷和後序遍歷都在此處了
        if (cur.right == pre || cur.right == null) {
            if (cur.right == null) {
                // 如果cur.right是null,那麼中序遍歷獲取結果
                res.get(1).add(cur.val);
            }
            // 後序獲取結果
            res.get(2).add(cur.val);
            // 後序遍歷,棧中元素可以彈出了,因為不需要再回來了
            stack.pop();
            // 更新後序遍歷上一次訪問的節點
            pre = cur;
            // cur要置空,因為要返回上一層,即取棧中元素
            cur = null;
        } else {
            // 第二次訪問,中序遍歷
            res.get(1).add(cur.val);
            cur = cur.right;
        }
    }
}

非遞迴版本的過程見圖:

cur.right 並未被真正的訪問到,或者說 cur.right == null 通過是否是null的判斷,實際上也算一次訪問了。

非遞迴版本遍歷的簡化

統一的模板是適用於所有的遞迴情況,如果只需要單獨的某個版本是可以簡化程式碼邏輯的。
如果是先序和中序遍歷,那麼只需要關心第一次訪問和第二次訪問即可,無需關心其他;而後序遍歷比較麻煩,需要關心是否是第三次返回的情況,所以各個非遞迴遍歷簡化版程式碼如下:

public void preOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    TreeNode cur = root;
    Deque<TreeNode> stack = new ArrayDeque<>();
    while (!stack.isEmpty() || cur != null) {
        //
        while (cur != null) {
            // 先序遍歷,第一次訪問該節點
            res.get(0).add(cur.val);
            stack.push(cur);
            cur = cur.left;
        }
        // cur 此時為null
        // 返回,即彈出棧中元素覆蓋即可
        cur = stack.pop();
        // 如果right節點是null也無妨,會繼續彈出
        cur = cur.right;
    }
}
public void inOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    TreeNode cur = root;
    Deque<TreeNode> stack = new ArrayDeque<>();
    while (!stack.isEmpty() || cur != null) {
        while (cur != null) {
            stack.push(cur);
            cur = cur.left;
        }
        cur = stack.pop();
        // 中序遍歷,第二次訪問該節點
        res.get(1).add(cur.val);
        cur = cur.right;
    }
}
public void postOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    TreeNode cur = root;
    TreeNode pre = null;
    Deque<TreeNode> stack = new ArrayDeque<>();
    while (!stack.isEmpty() || cur != null) {
        while (cur != null) {
            stack.push(cur);
            cur = cur.left;
        }
        // 後序遍歷比較麻煩,不能直接彈出,需要判斷是否是第三次返回
        cur = stack.peek();
        if (cur.right == pre || cur.right == null) {
            res.get(2).add(cur.val);
            stack.pop();
            pre = cur;
            cur = null;
        } else {
            cur = cur.right;
        }
    }
}

Morris遍歷

在有了對樹的遍歷的流程的認識後,簡單介紹一下二叉樹的Morris遍歷。
正如我們所見,二叉樹的遍歷,需要用到棧,即空間複雜度是O(logn),時間複雜度是O(n).
Morris遍歷可以實現二叉樹空間複雜度為O(1),時間複雜度也是O(n),他的原理是通過利用原樹中大量空閒指標的方式,達到節省空間的目的。

其基本的規則是:
假設訪問cur節點:

  1. 如果 cur 沒有左孩子,cur 向右移動 (cur = cur.right)
  2. 如果 cur 有左孩子,找到左孩子最右的非空節點 mostRight:
    a. 如果 mostRight 的右指標指向空,讓其指向 cur,然後 cur 向左移動 (cur = cur.left)
    b. 如果 mostRight 的右指標指向 cur,讓其指向 null,然後cur向右移動 (cur = cur.right)
  3. cur 為空時遍歷停止

根據規則,我們可以看到,通過 mostRight 節點的right指標,判斷是第幾次回到當前節點:

  1. 如果是指向null,那麼說明是第一次來到該節點;
  2. 如果指向自己,那麼說明是第二次回到當前節點。

Morris遍歷並沒有第三次回到當前節點的操作,所以需要逆序列印邊界。

時間複雜度是O(N),因為對於樹中的每個節點至多隻能被訪問兩次。

遍歷過程詳情圖和程式碼:

public class Morris {
    public static void main(String[] args) {

        TreeNode root = new TreeNode(4);
        root.left = new TreeNode(2);
        root.right = new TreeNode(6);
        root.left.left = new TreeNode(1);
        root.left.right = new TreeNode(3);
        root.right.left = new TreeNode(5);
        root.right.right = new TreeNode(7);
        traverseTree(root);
        System.out.println("=====");
        morris(root);
    }

    public static void traverseTree(TreeNode root) {
        if (root == null) {
            return;
        }
        // 先序
//        System.out.println(root.val);
        traverseTree(root.left);
        // 中序
//        System.out.println(root.val);
        traverseTree(root.right);
        // 後序
        System.out.println(root.val);
    }

    public static void morris(TreeNode root) {
        if (root == null) {
            return;
        }
        TreeNode cur = root;
        while (cur != null) {
            // 1. 沒有左孩子,cur向右邊移動
            if (cur.left == null) {
                // 這個也是第一次到,也是第二次到,對於先序和中序遍歷而言,都需要處理
//                System.out.println(cur.val);
                cur = cur.right;
            } else {
                // 2 有左孩子,找到左孩子最右的非空節點 mostRight
                TreeNode mostRight = cur.left;
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                // 判斷 mostRight
                if (mostRight.right == null) {
                    // a. mostRight 指向null,則讓其指向自己,向左邊走
                    mostRight.right = cur;
                    // 第一次到cur,先序遍歷
//                    System.out.println(cur.val);
                    cur = cur.left;
                } else {
                    // b. 如果 mostRight 的右指標指向 cur,讓其指向 null,然後cur向右移動
                    mostRight.right = null;
                    // 第二次到cur,中序遍歷
//                    System.out.println(cur.val);

                    // 後序遍歷,左右中,相當於是列印樹的 \ 這個樣子的右斜線,所以在第二次離開的時候,逆序列印右斜線 \ 即可。
                    // 為了使用O(1)的空間複雜度,可以先對樹的節點進行 "反轉連結串列" 的操作,然後列印完成之後,再反轉回來
                    printReverseRightEdge(cur.left);
                    cur = cur.right;
                }
            }
        }
        printReverseRightEdge(root);
    }

    private static void printReverseRightEdge(TreeNode root) {
        if (root == null) {
            return;
        }
        // 反轉列表
        TreeNode rHead = reverseRightEdge(root);
        // 列印
        TreeNode cur = rHead;
        while (cur != null) {
            System.out.println(cur.val);
            cur = cur.right;
        }
        // 反轉回來
        reverseRightEdge(rHead);
    }

    private static TreeNode reverseRightEdge(TreeNode root) {
        TreeNode pre = null;
        TreeNode cur = root;
        while (cur != null) {
            TreeNode next = cur.right;
            cur.right = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }
}

參考資料

Morris 遍歷圖,參考:https://blog.csdn.net/qq_38636076/article/details/119147902
牛客網左神演算法課程