【資料結構與演算法】二叉樹的 Morris 遍歷(前序、中序、後序)

gonghr發表於2021-10-09

前置說明

不瞭解二叉樹非遞迴遍歷的可以看我之前的文章【資料結構與演算法】二叉樹模板及例題

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;
   }

相關文章