二叉樹神級遍歷演算法——Morris遍歷(C++版)
題目:
設計一個演算法實現二叉樹的三種遍歷(前序遍歷 中序遍歷 後序遍歷)。
要求時間複雜度為O(n) 空間複雜度為O(1)。
思路:
空間複雜度O(1)的要求很嚴格。常規的遞迴實現是顯然不能滿足要求的[其空間複雜度是樹的深度O(h) ]。本篇文章介紹著名的Morris遍歷,該方法利用了二叉樹結點中大量指向null的指標。
常規的棧結構遍歷方式,遍歷到某個節點之後並不能回到上層的結點,這是由二叉樹本身的結構所限制的,每個結點並沒有指向父節點的指標,因此需要使用棧來完成回到上層結點的步驟。
Morris遍歷避免了使用棧結構,讓下層有指向上層的指標,但並不是所有的下層結點都有指向上層的指標([這些指標也稱為空閒指標
空閒指標的分配規則如下:
1. 當前子樹的頭結點為head,空閒指標由head的左子樹中最右結點的右指標指向head結點。對head的左子樹重複該步驟1,直到遍歷至某個結點沒有左子樹,將該結點記 為node。進入步驟2。
2. 從node結點開始通過每個結點的右指標進行移動,並列印結點的值。
假設遍歷到的當前結點為curNode,做如下判斷:
curNode結點的左子樹中最右結點(記為lastRNode)是否指向curNode。
A. 是 讓lastRNode結點的右指標指向null,列印curNode的值。接著通過curNode的右指標遍歷下一個結點,重複步驟2。
B. 否 將
下面舉例說明上述步驟(先以中序遍歷為例),二叉樹結構如下圖所示:
遍歷至結點1 發現其沒有左子樹 記為Node。
curNode : 1 列印1
curNode : 2 滿足A 空閒指標由1的右指標指向2 將該空閒指標取消掉 列印2。通過2的右指標遍歷到3。
curNode : 3 滿足B 進行步驟1 最終列印3。
通過空閒指標遍歷至4。
curNode : 4 滿足A 空閒指標由3的右指標指向4 將該空閒指標取消掉 列印4。通過4的右指標遍歷到6。
至此 左子樹和根結點遍歷完畢。
curNode : 6 滿足B 進行步驟1 之後二叉樹變為右圖。
遍歷至結點5
curNode : 5 滿足B 進行步驟1 最終列印5。
通過空閒指標遍歷至6。
curNode : 6 滿足A 空閒指標由5的右指標指向6 將該空閒指標取消掉 列印6。
通過6的右指標遍歷到7。
curNode : 7 滿足B 進行步驟1 最終列印7。
3. 步驟2最終移動到null結點 整個過程結束。
總結:
列印某個結點時,一定是在步驟2開始移動的過程中。
步驟2最開始從子樹最左結點開始,在通過右指標移動過程中,只有以下兩種移動方式:
①移動到某個結點的右子樹【此時 左子樹和根結點必定已經列印結束】
②移動到某個上層結點(即通過空閒指標移動)【此時 該上層結點的左子樹整體列印完畢 開始處理根結點】
Morris先序遍歷只需要將列印順序稍微調整一下(調整至步驟1中列印)。
Morris後序遍歷同樣是需要將列印順序稍微調整一下,即:逆序列印(不能使用額外的資料結構)所有結點的左子樹右邊界,在滿足步驟2中情況A時列印。
注:
二叉樹結點定義如下:
typedef int dataType;
struct Node
{
dataType val;
struct Node *left;
struct Node *right;
Node(dataType _val):
val(_val), left(NULL), right(NULL){}
};
常規的二叉樹遍歷方式採用棧實現,比較容易實現,下面直接給出程式碼。
/*************************Morris遍歷二叉樹*************************/
#include <iostream>
using namespace std;
typedef int dataType;
struct Node
{
dataType val;
struct Node *left;
struct Node *right;
Node(dataType _val):
val(_val), left(NULL), right(NULL){}
};
// Morris中序遍歷 (左 -> 根 -> 右)
void MorrisInOrderTraverse(Node *head)
{
if (head == NULL)
{
return;
}
Node *p1 = head;
Node *p2 = NULL;
while (p1 != NULL)
{
p2 = p1->left;
if (p2 != NULL)
{
while(p2->right != NULL && p2->right != p1)
{
p2 = p2->right;
}
if (p2->right == NULL)
{
p2->right = p1; // 空閒指標
p1 = p1->left;
continue;
}
else
{
p2->right = NULL;
}
}
cout<<p1->val<<" ";
p1 = p1->right;
}
}
// Morris前序遍歷 (根 -> 左 -> 右)
void MorrisPreOrderTraverse(Node *head)
{
if (head == NULL)
{
return;
}
Node *p1 = head;
Node *p2 = NULL;
while (p1 != NULL)
{
p2 = p1->left;
if (p2 != NULL)
{
while(p2->right != NULL && p2->right != p1)
{
p2 = p2->right;
}
if (p2->right == NULL)
{
p2->right = p1; // 空閒指標
cout<<p1->val<<" "; // 列印結點值的順序稍微調整
p1 = p1->left;
continue;
}
else
{
p2->right = NULL;
}
}
else
{
cout<<p1->val<<" ";
}
p1 = p1->right;
}
}
// 逆序右邊界
Node* reverseEdge(Node *head)
{
Node *pre = NULL;
Node *next = NULL;
while(head != NULL)
{
next = head->right;
head->right = pre;
pre = head;
head = next;
}
return pre;
}
// 逆序列印左子樹右邊界
void printEdge(Node *head)
{
Node *lastNode = reverseEdge(head);
Node *cur = lastNode;
while (cur != NULL)
{
cout<<cur->val<<" ";
cur = cur->right;
}
reverseEdge(lastNode);
}
// Morris後序遍歷 (左 -> 右 -> 根)
void MorrisPostOrderTraverse(Node *head)
{
if (head == NULL)
{
return;
}
Node *p1 = head;
Node *p2 = NULL;
while (p1 != NULL)
{
p2 = p1->left;
if (p2 != NULL)
{
while(p2->right != NULL && p2->right != p1)
{
p2 = p2->right;
}
if (p2->right == NULL)
{
p2->right = p1; // 空閒指標
p1 = p1->left;
continue;
}
else
{
p2->right = NULL;
printEdge(p1->left);
}
}
p1 = p1->right;
}
printEdge(head);
}
void buildBinTree(Node **head)
{
dataType _val;
cin>>_val;
if (_val == -1)
{
*head = NULL;
}
else
{
*head = (Node*)malloc(sizeof(Node));
(*head)->val = _val;
buildBinTree(&(*head)->left);
buildBinTree(&(*head)->right);
}
}
int main(void)
{
Node *head;
buildBinTree(&head);
cout<<"前序遍歷序列為:";
MorrisPreOrderTraverse(head);
cout<<endl;
cout<<"中序遍歷序列為:";
MorrisInOrderTraverse(head);
cout<<endl;
cout<<"後序遍歷序列為:";
MorrisPostOrderTraverse(head);
cout<<endl;
return 0;
}
/*************************Morris遍歷二叉樹 END*************************/
輸入:
1 2 3 -1 -1 4 -1 -1 5 6 8 -1 -1 -1 7 -1 -1
輸出:
致謝:
本篇文章參考自左神新書《程式設計師程式碼面試指南:IT名企演算法與資料結構題目最優解》,在此表示感謝。