本文主要解決一個問題,如何實現二叉樹的前中後序遍歷,有兩個要求:
O(1)空間複雜度,即只能使用常數空間;
二叉樹的形狀不能被破壞(中間過程允許改變其形狀)。
通常,實現二叉樹的前序(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組成:
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
中序遍歷
步驟:
如果當前節點的左孩子爲空,則輸出當前節點並將其右孩子作爲當前節點。
如果當前節點的左孩子不爲空,在當前節點的左子樹中找到當前節點在中序遍歷下的前驅節點。
a) 如果前驅節點的右孩子爲空,將它的右孩子設置爲當前節點。當前節點更新爲當前節點的左孩子。
b) 如果前驅節點的右孩子爲當前節點,將它的右孩子重新設爲空(恢復樹的形狀)。輸出當前節點。當前節點更新爲當前節點的右孩子。
重複以上1、2直到當前節點爲空。
圖示:
下圖爲每一步迭代的結果(從左至右,從上到下),cur代表當前節點,深色節點表示該節點已輸出。
代碼:
void inorderMorrisTraversal(TreeNode *root) {
TreeNode *cur = root, *prev = NULL;
while (cur != NULL)
{
if (cur->left == NULL) // 1.
{
printf("%d ", cur->val);
cur = cur->right;
}
else
{
// find predecessor
prev = cur->left;
while (prev->right != NULL && prev->right != cur)
prev = prev->right;
if (prev->right == NULL) // 2.a)
{
prev->right = cur;
cur = cur->left;
}
else // 2.b)
{
prev->right = NULL;
printf("%d ", cur->val);
cur = cur->right;
}
}
}
}
複雜度分析:
空間複雜度:O(1),因爲只用了兩個輔助指針。
時間複雜度:O(n)。證明時間複雜度爲O(n),最大的疑惑在於尋找中序遍歷下二叉樹中所有節點的前驅節點的時間複雜度是多少,即以下兩行代碼:
while (prev->right != NULL && prev->right != cur)
prev = prev->right;
直覺上,認爲它的複雜度是O(nlgn),因爲找單個節點的前驅節點與樹的高度有關。但事實上,尋找所有節點的前驅節點只需要O(n)時間。n個節點的二叉樹中一共有n-1條邊,整個過程中每條邊最多隻走2次,一次是爲了定位到某個節點,另一次是爲了尋找上面某個節點的前驅節點,如下圖所示,其中紅色是爲了定位到某個節點,黑色線是爲了找到前驅節點。所以複雜度爲O(n)。
前序遍歷與中序遍歷相似,代碼上只有一行不同,不同就在於輸出的順序。
前序遍歷
前序遍歷與中序遍歷相似,代碼上只有一行不同,不同就在於輸出的順序。
步驟:
如果當前節點的左孩子爲空,則輸出當前節點並將其右孩子作爲當前節點。
如果當前節點的左孩子不爲空,在當前節點的左子樹中找到當前節點在中序遍歷下的前驅節點。
a) 如果前驅節點的右孩子爲空,將它的右孩子設置爲當前節點。輸出當前節點(在這裏輸出,這是與中序遍歷唯一一點不同)。當前節點更新爲當前節點的左孩子。
b) 如果前驅節點的右孩子爲當前節點,將它的右孩子重新設爲空。當前節點更新爲當前節點的右孩子。
重複以上1、2直到當前節點爲空。
圖示:
void preorderMorrisTraversal(TreeNode *root) {
TreeNode *cur = root, *prev = NULL;
while (cur != NULL)
{
if (cur->left == NULL)
{
printf("%d ", cur->val);
cur = cur->right;
}
else
{
prev = cur->left;
while (prev->right != NULL && prev->right != cur)
prev = prev->right;
if (prev->right == NULL)
{
printf("%d ", cur->val); // the only difference with inorder-traversal
prev->right = cur;
cur = cur->left;
}
else
{
prev->right = NULL;
cur = cur->right;
}
}
}
}
複雜度分析:
時間複雜度與空間複雜度都與中序遍歷時的情況相同。
後序遍歷
後續遍歷稍顯複雜,需要建立一個臨時節點dump,令其左孩子是root。並且還需要一個子過程,就是倒序輸出某兩個節點之間路徑上的各個節點。
步驟:
當前節點設置爲臨時節點dump。
如果當前節點的左孩子爲空,則將其右孩子作爲當前節點。
如果當前節點的左孩子不爲空,在當前節點的左子樹中找到當前節點在中序遍歷下的前驅節點。
a) 如果前驅節點的右孩子爲空,將它的右孩子設置爲當前節點。當前節點更新爲當前節點的左孩子。
b) 如果前驅節點的右孩子爲當前節點,將它的右孩子重新設爲空。倒序輸出從當前節點的左孩子到該前驅節點這條路徑上的所有節點。當前節點更新爲當前節點的右孩子。
重複以上1、2直到當前節點爲空。
圖示:
void reverse(TreeNode *from, TreeNode *to) // reverse the tree nodes 'from' -> 'to'.
{
if (from == to)
return;
TreeNode *x = from, *y = from->right, *z;
while (true)
{
z = y->right;
y->right = x;
x = y;
y = z;
if (x == to)
break;
}
}
void printReverse(TreeNode* from, TreeNode *to) // print the reversed tree nodes 'from' -> 'to'.
{
reverse(from, to);
TreeNode *p = to;
while (true)
{
printf("%d ", p->val);
if (p == from)
break;
p = p->right;
}
reverse(to, from);
}
void postorderMorrisTraversal(TreeNode *root) {
TreeNode dump(0);
dump.left = root;
TreeNode *cur = &dump, *prev = NULL;
while (cur)
{
if (cur->left == NULL)
{
cur = cur->right;
}
else
{
prev = cur->left;
while (prev->right != NULL && prev->right != cur)
prev = prev->right;
if (prev->right == NULL)
{
prev->right = cur;
cur = cur->left;
}
else
{
printReverse(cur->left, prev); // call print
prev->right = NULL;
cur = cur->right;
}
}
}
}
複雜度分析:
空間複雜度同樣是O(1);時間複雜度也是O(n),倒序輸出過程只不過是加大了常數係數。
轉載自:
http://www.cnblogs.com/AnnieKim/archive/2013/06/15/MorrisTraversal.html。