1. 程式人生 > >Morris Traversal方法遍歷二叉樹(非遞迴,不用棧,O(1)空間)

Morris Traversal方法遍歷二叉樹(非遞迴,不用棧,O(1)空間)

本文主要解決一個問題,如何實現二叉樹的前中後序遍歷,有兩個要求:

1. O(1)空間複雜度,即只能使用常數空間;

2. 二叉樹的形狀不能被破壞(中間過程允許改變其形狀)。

通常,實現二叉樹的前序(preorder)、中序(inorder)、後序(postorder)遍歷有兩個常用的方法:一是遞迴(recursive),二是使用棧實現的迭代版本(stack+iterative)。這兩種方法都是O(n)的空間複雜度(遞迴本身佔用stack空間或者使用者自定義的stack),所以不滿足要求。(用這兩種方法實現的中序遍歷實現可以參考這裡。)

Morris Traversal方法可以做到這兩點,與前兩種方法的不同在於該方法只需要O(1)空間,而且同樣可以在O(n)時間內完成。

要使用O(1)空間進行遍歷,最大的難點在於,遍歷到子節點的時候怎樣重新返回到父節點(假設節點中沒有指向父節點的p指標),由於不能用棧作為輔助空間。為了解決這個問題,Morris方法用到了線索二叉樹(threaded binary tree)的概念。在Morris方法中不需要為每個節點額外分配指標指向其前驅(predecessor)和後繼節點(successor),只需要利用葉子節點中的左右空指標指向某種順序遍歷下的前驅節點或後繼節點就可以了。

Morris只提供了中序遍歷的方法,在中序遍歷的基礎上稍加修改可以實現前序,而後續就要再費點心思了。所以先從中序開始介紹。

首先定義在這篇文章中使用的二叉樹節點結構,即由val,left和right組成:

1 struct TreeNode {
2     int val;
3     TreeNode *left;
4     TreeNode *right;
5     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
6 };

一、中序遍歷

步驟:

1. 如果當前節點的左孩子為空,則輸出當前節點並將其右孩子作為當前節點。

2. 如果當前節點的左孩子不為空,在當前節點的左子樹中找到當前節點在中序遍歷下的前驅節點。

   a) 如果前驅節點的右孩子為空,將它的右孩子設定為當前節點。當前節點更新為當前節點的左孩子。

   b) 如果前驅節點的右孩子為當前節點,將它的右孩子重新設為空(恢復樹的形狀)。輸出當前節點。當前節點更新為當前節點的右孩子。

3. 重複以上1、2直到當前節點為空。

圖示:

下圖為每一步迭代的結果(從左至右,從上到下),cur代表當前節點,深色節點表示該節點已輸出。

程式碼:

 1 void inorderMorrisTraversal(TreeNode *root) {
 2     TreeNode *cur = root, *prev = NULL;
 3     while (cur != NULL)
 4     {
 5         if (cur->left == NULL)          // 1.
 6         {
 7             printf("%d ", cur->val);
 8             cur = cur->right;
 9         }
10         else
11         {
12             // find predecessor
13             prev = cur->left;
14             while (prev->right != NULL && prev->right != cur)
15                 prev = prev->right;
16 
17             if (prev->right == NULL)   // 2.a)
18             {
19                 prev->right = cur;
20                 cur = cur->left;
21             }
22             else                       // 2.b)
23             {
24                 prev->right = NULL;
25                 printf("%d ", cur->val);
26                 cur = cur->right;
27             }
28         }
29     }
30 }

複雜度分析:

空間複雜度:O(1),因為只用了兩個輔助指標。

時間複雜度:O(n)。證明時間複雜度為O(n),最大的疑惑在於尋找中序遍歷下二叉樹中所有節點的前驅節點的時間複雜度是多少,即以下兩行程式碼:

1 while (prev->right != NULL && prev->right != cur)
2     prev = prev->right;

直覺上,認為它的複雜度是O(nlgn),因為找單個節點的前驅節點與樹的高度有關。但事實上,尋找所有節點的前驅節點只需要O(n)時間。n個節點的二叉樹中一共有n-1條邊,整個過程中每條邊最多隻走2次,一次是為了定位到某個節點,另一次是為了尋找上面某個節點的前驅節點,如下圖所示,其中紅色是為了定位到某個節點,黑色線是為了找到前驅節點。所以複雜度為O(n)。

二、前序遍歷

前序遍歷與中序遍歷相似,程式碼上只有一行不同,不同就在於輸出的順序。

步驟:

1. 如果當前節點的左孩子為空,則輸出當前節點並將其右孩子作為當前節點。

2. 如果當前節點的左孩子不為空,在當前節點的左子樹中找到當前節點在中序遍歷下的前驅節點。

   a) 如果前驅節點的右孩子為空,將它的右孩子設定為當前節點。輸出當前節點(在這裡輸出,這是與中序遍歷唯一一點不同)。當前節點更新為當前節點的左孩子。

   b) 如果前驅節點的右孩子為當前節點,將它的右孩子重新設為空。當前節點更新為當前節點的右孩子。

3. 重複以上1、2直到當前節點為空。

圖示:

程式碼:

 1 void preorderMorrisTraversal(TreeNode *root) {
 2     TreeNode *cur = root, *prev = NULL;
 3     while (cur != NULL)
 4     {
 5         if (cur->left == NULL)
 6         {
 7             printf("%d ", cur->val);
 8             cur = cur->right;
 9         }
10         else
11         {
12             prev = cur->left;
13             while (prev->right != NULL && prev->right != cur)
14                 prev = prev->right;
15 
16             if (prev->right == NULL)
17             {
18                 printf("%d ", cur->val);  // the only difference with inorder-traversal
19                 prev->right = cur;
20                 cur = cur->left;
21             }
22             else
23             {
24                 prev->right = NULL;
25                 cur = cur->right;
26             }
27         }
28     }
29 }

複雜度分析:

時間複雜度與空間複雜度都與中序遍歷時的情況相同。

三、後序遍歷

後續遍歷稍顯複雜,需要建立一個臨時節點dump,令其左孩子是root。並且還需要一個子過程,就是倒序輸出某兩個節點之間路徑上的各個節點。

步驟:

當前節點設定為臨時節點dump。

1. 如果當前節點的左孩子為空,則將其右孩子作為當前節點。

2. 如果當前節點的左孩子不為空,在當前節點的左子樹中找到當前節點在中序遍歷下的前驅節點。

   a) 如果前驅節點的右孩子為空,將它的右孩子設定為當前節點。當前節點更新為當前節點的左孩子。

   b) 如果前驅節點的右孩子為當前節點,將它的右孩子重新設為空。倒序輸出從當前節點的左孩子到該前驅節點這條路徑上的所有節點。當前節點更新為當前節點的右孩子。

3. 重複以上1、2直到當前節點為空。

圖示:

程式碼:

 1 void reverse(TreeNode *from, TreeNode *to) // reverse the tree nodes 'from' -> 'to'.
 2 {
 3     if (from == to)
 4         return;
 5     TreeNode *x = from, *y = from->right, *z;
 6     while (true)
 7     {
 8         z = y->right;
 9         y->right = x;
10         x = y;
11         y = z;
12         if (x == to)
13             break;
14     }
15 }
16 
17 void printReverse(TreeNode* from, TreeNode *to) // print the reversed tree nodes 'from' -> 'to'.
18 {
19     reverse(from, to);
20     
21     TreeNode *p = to;
22     while (true)
23     {
24         printf("%d ", p->val);
25         if (p == from)
26             break;
27         p = p->right;
28     }
29     
30     reverse(to, from);
31 }
32 
33 void postorderMorrisTraversal(TreeNode *root) {
34     TreeNode dump(0);
35     dump.left = root;
36     TreeNode *cur = &dump, *prev = NULL;
37     while (cur)
38     {
39         if (cur->left == NULL)
40         {
41             cur = cur->right;
42         }
43         else
44         {
45             prev = cur->left;
46             while (prev->right != NULL && prev->right != cur)
47                 prev = prev->right;
48 
49             if (prev->right == NULL)
50             {
51                 prev->right = cur;
52                 cur = cur->left;
53             }
54             else
55             {
56                 printReverse(cur->left, prev);  // call print
57                 prev->right = NULL;
58                 cur = cur->right;
59             }
60         }
61     }
62 }

複雜度分析:

空間複雜度同樣是O(1);時間複雜度也是O(n),倒序輸出過程只不過是加大了常數係數。

注:

以上所有的程式碼以及測試程式碼可以在我的Github裡獲取。

參考:

http://www.geeksforgeeks.org/inorder-tree-traversal-without-recursion-and-without-stack/
http://www.geeksforgeeks.org/morris-traversal-for-preorder/
http://stackoverflow.com/questions/6478063/how-is-the-complexity-of-morris-traversal-on
http://blog.csdn.net/wdq347/article/details/8853371
Data Structures and Algorithms in C++ by Adam Drozdek

---------------

以前我只知道遞迴和棧+迭代實現二叉樹遍歷的方法,昨天才瞭解到有使用O(1)空間複雜度的方法。以上都是我參考了網上的資料加上個人的理解來總結,如果有什麼不對的地方非常歡迎大家的指正。

原創文章,歡迎轉載,轉載請註明出處:http://www.cnblogs.com/AnnieKim/archive/2013/06/15/MorrisTraversal.html。