二叉樹的深度優先遍歷之先序中序後序遞迴非遞迴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); }
如程式碼所示:
- 先序遍歷是剛走到該節點便獲取該節點的值,然後遞迴遍歷左子樹、右子樹(
中左右
); - 中序遍歷是遞迴遍歷完左子樹,然後獲取該節點的值,最後再遞迴遍歷右子樹(
左中右
); - 後序遍歷是遞迴遍歷完左子樹,再遞迴遍歷右子樹,最後獲取該節點的值(
左右中
);
注:上述程式碼中res的宣告為:List<List<Integer>> res
;
如圖所示:
圖中描述了遞迴在樹的各個節點之間的抽象過程:
圖形說明:
- 箭頭指向表示遞迴函式訪問到該節點;
- N表示null節點,指向null節點的指標,表示遞迴函式進入到null節點的判斷處。
結合遞迴函式看該圖就很簡單了:
- 先序遍歷,就是第一個箭頭指向時列印的地方(中左右,從上往下,剛進入到該節點就獲取該節點的值)
- 中序遍歷,就是第二個箭頭指向時列印的地方(左中右,遍歷左邊節點返回,然後再獲取該節點的值,然後訪問右節點)
- 後序遍歷,就是第三個箭頭指向時列印的地方(左右中,遍歷完成左、右兩邊節點後返回,最後再獲取該節點的值)
非遞迴版本遍歷的統一模板
前邊圖形描述了遞迴版本的過程,非遞迴版本即模擬該過程即可:
詳見程式碼:
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節點:
- 如果 cur 沒有左孩子,cur 向右移動 (cur = cur.right)
- 如果 cur 有左孩子,找到左孩子最右的非空節點 mostRight:
a. 如果 mostRight 的右指標指向空,讓其指向 cur,然後 cur 向左移動 (cur = cur.left)
b. 如果 mostRight 的右指標指向 cur,讓其指向 null,然後cur向右移動 (cur = cur.right) - cur 為空時遍歷停止
根據規則,我們可以看到,通過 mostRight 節點的right指標,判斷是第幾次回到當前節點:
- 如果是指向null,那麼說明是第一次來到該節點;
- 如果指向自己,那麼說明是第二次回到當前節點。
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
牛客網左神演算法課程