前置說明
不瞭解二叉樹非遞迴遍歷的可以看我之前的文章【資料結構與演算法】二叉樹模板及例題
Morris 遍歷
概述
Morris 遍歷是一種遍歷二叉樹的方式,並且時間複雜度O(N),額外空間複雜度O(1) 。通過利用原樹中大量空閒指標的方式,達到節省空間的目的
分析
設一棵二叉樹有 n 個節點,則所有節點的指標域總和為 2 * n ,所有節點的非空指標域總和為 n - 1(非根節點被一個指標指向,根節點不被指標指向),所有節點的空指標域總和為 2n - (n - 1) = n + 1。
可以看到有大量的空指標域沒有用到,在可以改變原二叉樹結構的前提下,我們可以通過合理利用節點的空指標域,不開闢額外空間進行二叉樹的非遞迴遍歷。
❓ 那麼先序、中序、後序遍歷的節點訪問順序是如何確定的呢
如上圖,根據紫色箭頭順序訪問,第一次訪問到的節點組成的集合就是先序遍歷的結果。類似的,第二次訪問到的節點組成的集合就是中序遍歷的結果;第三次訪問到的節點組成的集合就是後序遍歷的結果。
通過設定節點訪問不同次數的操作就可以實現三種遍歷。
❓ Morris 遍歷的實質:建立一種機制,對於沒有左子樹的節點只到達一次,對於有左子樹的節點會到達兩次
? Morris 遍歷的原則
假設來到當前節點 cur,開始時 cur 來到頭節點位置
-
如果 cur 沒有左孩子,cur向右移動(cur = cur.right)
-
如果 cur 有左孩子,找到左子樹上最右的節點 mostRight
-
a.如果 mostRight 的右指標指向空,讓其指向 cur, 然後 cur 向左移動(cur = cur.left)
-
b.如果 mostRight 的右指標指向 cur,讓其指向 null, 然後 cur 向右移動(cur = cur.right)
-
-
cur 為空時遍歷停止
? 舉個例子:
1️⃣ 首先 cur 來到頭結點 1,按照 morris 原則的第二條第一點,它存在左孩子,cur 左子樹上最右的節點為 5,它的 right 指標指向空,所以讓其指向 1,cur 向左移動到2。
2️⃣ 2 有左孩子,且它左子樹最右的節點 4 指向空,按照 morris 原則的第二條第一點,讓 4 的 right 指標指向 2,cur 向左移動到 4
3️⃣ 4 不存在左孩子,按照 morris 原則的第一條,cur 向右移動,在第二步中,4 的 right 指標已經指向了 2,所以 cur 會回到 2
4️⃣ 重新回到 2,有左孩子,它左子樹最右的節點為 4,但是在第二步中,4 的 right 指標已經指向了 2,不為空。所以按照 morris 原則的第二條第二點,cur 向右移動到 5,同時 4 的 right 指標重新指向空
5️⃣ 5 不存在左孩子,按照 morris 原則的第一條,cur 向右移動,在第一步中,5 的 right 指標已經指向了 1,所以 cur 會回到 1
6️⃣ cur 回到 1,回到頭結點,左子樹遍歷完成,1 有左孩子,左子樹上最右的節點為 5,它的 right 指標指向 1,按照 morris 原則的第二條第二點,1 向右移動到 3,同時 5 的 right 指標重新指回空
7️⃣ 3 有左孩子,且它左子樹最右的節點 6 指向空,按照 morris 原則的第二條第一點,讓 6 的 right 指標指向 3,cur 向左移動到 6
8️⃣ 6 不存在左孩子,按照 morris 原則的第一條,cur 向右移動,在第二步中,6 的 right 指標已經指向了 3,所以 cur 會回到 3
9️⃣ 重新回到 3,有左孩子,它左子樹最右的節點為 6,但是在第二步中,6 的 right 指標已經指向了 3,不為空。所以按照 morris 原則的第二條第二點,cur 向右移動到 7,同時 6 的 right 指標重新指向空
1️⃣0️⃣ cur 沒有左孩子,向右移動到 null,遍歷停止
以上就是 Morris 遍歷的全過程了,通過在遍歷過程中適當的位置,即每個節點訪問特定次數後設定操作,可以實現三種遍歷
前序遍歷
-
對於沒有左子樹的節點只到達一次,直接列印
-
對於有左子樹的節點會到達兩次,則在第一次到達時列印
public static void morrisPre(Node head) {
if(head == null){
return;
}
Node cur = head;
Node mostRight = null;
while (cur != null){
// cur表示當前節點,mostRight表示cur的左孩子的最右節點
mostRight = cur.left;
if(mostRight != null){
// cur有左孩子,找到cur左子樹最右節點
while (mostRight.right !=null && mostRight.right != cur){
mostRight = mostRight.right;
}
// mostRight的右孩子指向空,讓其指向cur,cur向左移動
if(mostRight.right == null){
mostRight.right = cur;
System.out.print(cur.value+" "); // 此時第一次訪問節點
cur = cur.left;
continue; // 直接進入下一次迴圈
}else {
// mostRight的右孩子指向cur,讓其指向空,cur向右移動
mostRight.right = null;
}
}else {
System.out.print(cur.value + " "); // 沒有左孩子的話直接輸出,該節點就是第一次訪問
}
cur = cur.right;
}
System.out.println();
}
中序遍歷
-
對於沒有左子樹的節點只到達一次,直接列印
-
對於有左子樹的節點會到達兩次,第二次到達時列印
public static void morrisIn(Node head) {
if(head == null){
return;
}
Node cur = head;
Node mostRight = null;
while (cur != null){
// cur表示當前節點,mostRight表示cur的左孩子的最右節點
mostRight = cur.left;
if(mostRight != null){
// cur有左孩子,找到cur左子樹最右節點
while (mostRight.right !=null && mostRight.right != cur){
mostRight = mostRight.right;
}
// mostRight的右孩子指向空,讓其指向cur,cur向左移動
if(mostRight.right == null){
mostRight.right = cur;
cur = cur.left;
continue; // 直接進入下一次迴圈
}else { // 第二次到達
// mostRight的右孩子指向cur,讓其指向空,cur向右移動
mostRight.right = null;
}
}
System.out.print(cur.value+" "); // 沒有左子樹的節點只到達一次直接列印,對於有左子樹的節點會到達兩次,第二次到達時列印
cur = cur.right;
}
System.out.println();
}
後序遍歷
-
第二次訪問節點時逆序列印該節點左樹的右邊界
-
最後單獨列印整棵樹的右邊界
public static void morrisPos(Node head) {
if(head == null){
return;
}
Node cur = head;
Node mostRight = null;
while (cur != null){
mostRight = cur.left;
if(mostRight != null){
while (mostRight.right !=null && mostRight.right != cur){
mostRight = mostRight.right;
}
if(mostRight.right == null){
mostRight.right = cur;
cur = cur.left;
continue;
}else {
mostRight.right = null;
printEdge(cur.left); // 第二次訪問時逆序列印該節點左樹的右邊界
}
}
cur = cur.right;
}
printEdge(head); // 最後單獨列印整棵樹的右邊界
System.out.println();
}
public static void printEdge(Node node){ // 逆序列印:反轉連結串列列印後再反轉回原樣
Node tail =reverseEdge(node);
Node cur = tail;
while (cur != null ){
System.out.print(cur.value+" ");
cur =cur.right;
}
reverseEdge(tail);
}
public static Node reverseEdge(Node node){ // 連結串列反轉
Node pre = null;
Node next = null;
while (node != null){
next = node.right;
node.right = pre;
pre = node;
node = next;
}
return pre;
}