一、Morris遍思路
Mirros遍歷思路:
cur 初始指向頭結點
1) 如果 cur 無左孩子,則cur向右移動 (cur = cur->right);
2) 如果 cur 有左孩子,則找到其左子樹最右的節點,姑且叫 mostright
A: 如果mostright的right指針指向空,則讓其指向cur,然後cur向左移動 (cur = cur->left);
B: 如果mostright的right指向cur,則讓其指向空,cur向右移動 (cur = cur->right);
具體流程可以參考下面圖示步驟(手動模糊~~):
分析上面cur的走勢就可以知道:cur走過的節點和順序爲:1,2,4,2,5,1,3,6,3,7.
實際上Morris遍歷實際上會將所有具有左子樹的節點過兩次,所有右節點只過一次
理解了上述遍歷方式代碼實現就很容易理解了:
class TreeNode {
public:
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int data) :val(val), left(NULL), right(NULL) {
}
};
void morris(TreeNode* head) {
if (head == NULL)
return;
TreeNode* cur = head;
TreeNode* mostRight = NULL;
while (cur != NULL) {
mostRight = cur->left;//來的cur的左子樹,然後找到左子樹最右邊的節點
if (mostRight != NULL) {//if條件成立則存在左子樹
while (mostRight->right != NULL && mostRight->right != cur) {//找左子樹最右的節點
mostRight = mostRight->right;//直到mostRight指向空或者指向cur則停止
}
if (mostRight->right == NULL) {//左子樹最右節點的right指針指向空,則使其指向cur,cur向左邊移動
mostRight->right = cur;
cur = cur->left;
continue;//更新這個cur之後則回到循環入口繼續這個過程
}
else {//否則mostRight的right指針指向cur本身,則讓mostRight的right指針指向空
mostRight->right = NULL;
}
}
cur = cur->right;
}
}
二、Morris遍歷和遞歸遍歷的關係
分析完morris遍歷方法,我們對比一下我們很熟悉來的遞歸比遍歷,下面仔細分析一下遞歸遍歷的過程:
void process(TreeNode* root){
process(TreeNode* root->left);
process(TreeNode* root->right);
}
現在看一下下面這顆二叉樹的遞歸遍歷過程:從下圖手繪過程可以分析遞歸過程,每一個節點一共訪問了3次,所以遞歸遍歷的訪問順序爲:【1 2 2 2 1 3 3 3 1】遍歷順序都是一樣的,由於打印時機的不同分成了先序,中序和後續而已。
先序:將打印時機放在第一次訪問節點的時候,就是在開始遍歷左子樹前:即 1 2 3
void process(TreeNode* root) {
cout << root->val;
process(root->left);
process(root->right);
}
中序:將打印時機放在第二次訪問節點的時候,看上面手繪圖,第二次訪問節點的時刻就是跑跑完左子樹遍歷回來的時候:即 2 1 3
void process(TreeNode* root) { process(root->left); cout << root->val; process(root->right); }
後序:將打印時機放在第三次訪問節點的時候,看上面手繪圖,第三次訪問節點的時刻就是跑跑完右子樹遍歷回來的時候:即 2 3 1
void process(TreeNode* root) {
process(root->left);
process(root->right);
cout << root->val;
}
而對於Morris遍歷,只要節點存在左子樹就可以回到該節點兩次,所以實際上相當於是一個閹割版本的遞歸遍歷。
三、Morris的前中後序遍歷實現
而對於Morris遍歷,如果節點存在左子樹那麼會訪問該節點兩次,所以肯定可以將其改爲先序遍歷和中序遍歷。
Morris遍歷的先序分析結合上面的Morris代碼(關注註釋部分):
void preMorris(TreeNode* head) {
if (head == NULL)
return;
TreeNode* cur = head;
TreeNode* 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) {//cur移動到其左子樹前是第一次來到該節點位置
mostRight->right = cur;
cout << cur->val<<' ';
cur = cur->left;
continue;
}
else {
mostRight->right = NULL;
}
}
else {//如果不存在左子樹,向右邊移動之前,直接輸出cur值
cout << cur->val << ' ';
}
cur = cur->right;
}
}
Morris遍歷的中序分析(關注註釋部分):
void inMorris(TreeNode* head) {
if (head == NULL)
return;
TreeNode* cur = head;
TreeNode* mostRight = NULL;
while (cur != NULL) {
mostRight = cur->left;
if (mostRight != NULL) {//進入條件,表示存在左子樹,那麼一個節點有左子樹的時候,第2次來到左子樹位置是什麼時候呢??
while (mostRight->right != NULL && mostRight->right != cur) {
mostRight = mostRight->right;
}
if (mostRight->right == NULL) {
mostRight->right = cur;
cur = cur->left;
continue;
}
else {//else滿足條件表示mostRight的right指向cur自己,也就是第二次來到cur的時候,即第二次訪問節點的時候
mostRight->right = NULL;//第二次來到cur的時候就是mostRight的右子樹指向自己的時
cout << cur->val << ' ';
}
}
else {//如果不存在左子樹,向右邊移動之前,直接輸出cur值,也就是根節點值
cout << cur->val << ' ';
}
cur = cur->right;
}
}
代碼優化如下:如果一個節點有左子樹的時候,我們將打印時機放在最後一次來到這個節點的時候,如果有左子樹要把左子樹處理完再打印當前節點的值,如果一個節點沒有左子樹則第一次和第二次重疊,所以將打印時機放在cur往右邊移動之前即可。
void inMorris(TreeNode* head) {
if (head == NULL)
return;
TreeNode* cur = head;
TreeNode* 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;
}
}
cout << cur->val << ' ';//處理完左子樹之後再打印當前節點
cur = cur->right;
}
}
Morris遍歷實現後續遍歷,但是Morris沒有第三次回到一個節點,那麼怎麼他是怎麼做到後續遍歷的?
1)後續遍歷只關心能夠回到節點兩次的節點,然後在第二次訪問節點的時候逆序打印其左子樹的右邊界。
如下圖,只考慮能兩次到達的節點,只有1 2 3三個節點。然後逆序打印其左子樹的右邊界,第二次訪問2的時候打印其左子樹的右邊界4,第二次訪問1的時候左子樹的右邊界爲2,5逆序打印他(5 ,2),然後第二次訪問3的時候左子樹右邊界爲6,經過第一步就可以得到:【4,5,2,6】
2)完成第一步之後,在整個函數退出之前,單獨逆序打印整顆數的右邊界。
整顆數的右邊界爲【1 3 7】,逆序打印結果【7 3 1】
最後:將步驟1+2輸出:【4 2 5 6 7 3 1】。
實現代碼(注意註釋部分):
void postMorris(TreeNode* head) {
if (head == NULL)
return;
TreeNode* cur = head;
TreeNode* mostRight = NULL;
while (cur != NULL) {
mostRight = cur->left;
if (mostRight != NULL) {//if滿足條件表示存在左子樹
while (mostRight->right != NULL && mostRight->right != cur) {
mostRight = mostRight->right;
}
if (mostRight->right == NULL) {
mostRight->right = cur;
cur = cur->left;
continue;
}
else {//進入else條件表示mostRight的right指向cur自己,表示cur第二次來到自己的時候
mostRight->right = NULL;
reversePrintEdge(cur->left);//第二次訪問該節點的時候逆序打印其左子樹的右邊界
}
}
cur = cur->right;
}
reversePrintEdge(head);//在整個函數退出之前逆序打印整棵樹的右邊界
}
思考:如何實現逆序打印右邊界並實現複雜度爲O(1),如果使用棧那麼會佔用更多額外空間複雜度。
答案:使用逆轉鏈表的的方法進,打印完成之後再將其逆轉回去,完成逆序打印。
//額外空間複雜度爲O(1)
void reversePrintEdge(TreeNode* root) {
if (root == NULL)
return;
//逆轉鏈表,使用三個指針,cur,pre和temp;
TreeNode* pre = NULL;
TreeNode* cur = root;
TreeNode* temp = cur;
while (cur != NULL) {
cur = cur->right;
temp->right = pre;//修改temp的指向
pre = temp;//然後更新pre
temp = cur;//然後更新temp
}
TreeNode* tail = pre;//最後尾節點指向pre
//逆轉之後直接打印即可實現逆序輸出
cur = tail;
while (cur != NULL) {
cout << cur->val<<' ';
cur = cur->right;
}
//然後再將鏈表逆序回來,調整爲原來的樹結構
cur = tail;
temp = tail;
pre = NULL;
while (cur != NULL) {
cur = cur->right;
temp->right = pre;
pre = temp;
temp = cur;
}
}
完整測試代碼:
#include<iostream>
using namespace std;
class TreeNode {
public:
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int data) :val(data), left(NULL), right(NULL) {
}
};
void reversePrintEdge(TreeNode*);
//先序Morris遍歷
void preMorris(TreeNode* head) {
if (head == NULL)
return;
TreeNode* cur = head;
TreeNode* 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) {//cur移動到其左子樹前是第一次來到該節點位置
mostRight->right = cur;
cout << cur->val << ' ';
cur = cur->left;
continue;
}
else {
mostRight->right = NULL;
}
}
else {//如果不存在左子樹,向右邊移動之前,直接輸出cur值
cout << cur->val << ' ';
}
cur = cur->right;
}
}
//中序Morris遍歷
void inMorris(TreeNode* head) {
if (head == NULL)
return;
TreeNode* cur = head;
TreeNode* 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;
}
}
cout << cur->val << ' ';//處理完左子樹之後再打印當前節點
cur = cur->right;
}
}
//後序Morris遍歷
void postMorris(TreeNode* head) {
if (head == NULL)
return;
TreeNode* cur = head;
TreeNode* mostRight = NULL;
while (cur != NULL) {
mostRight = cur->left;
if (mostRight != NULL) {//if滿足條件表示存在左子樹
while (mostRight->right != NULL && mostRight->right != cur) {
mostRight = mostRight->right;
}
if (mostRight->right == NULL) {
mostRight->right = cur;
cur = cur->left;
continue;
}
else {//進入else條件表示mostRight的right指向cur自己,表示cur第二次來到自己的時候
mostRight->right = NULL;
reversePrintEdge(cur->left);//第二次訪問該節點的時候逆序打印其左子樹的右邊界
}
}
cur = cur->right;
}
reversePrintEdge(head);//在整個函數退出之前逆序打印整棵樹的右邊界
}
//額外空間複雜度爲O(1)
void reversePrintEdge(TreeNode* root) {
if (root == NULL)
return;
//逆轉鏈表,使用三個指針,cur,pre和temp;
TreeNode* pre = NULL;
TreeNode* cur = root;
TreeNode* temp = cur;
while (cur != NULL) {
cur = cur->right;
temp->right = pre;//修改temp的指向
pre = temp;//然後更新pre
temp = cur;//然後更新temp
}
TreeNode* tail = pre;//最後尾節點指向pre
//逆轉之後直接打印即可實現逆序輸出
cur = tail;
while (cur != NULL) {
cout << cur->val << ' ';
cur = cur->right;
}
//然後再將鏈表逆序回來,調整爲原來的樹結構
cur = tail;
temp = tail;
pre = NULL;
while (cur != NULL) {
cur = cur->right;
temp->right = pre;
pre = temp;
temp = cur;
}
}
//遞歸先序遍歷
void preProcess(TreeNode* root) {
if (root != NULL) {
cout << root->val << ' ';
preProcess(root->left);
preProcess(root->right);
}
}
//遞歸中序遍歷
void inProcess(TreeNode* root) {
if (root != NULL) {
inProcess(root->left);
cout << root->val << ' ';
inProcess(root->right);
}
}
//遞歸後續遍歷
void postProcess(TreeNode* root) {
if (root != NULL) {
postProcess(root->left);
postProcess(root->right);
cout << root->val << ' ';
}
}
//創建節點
TreeNode* createTreeNode()
{
TreeNode *p;
int data;
cin >> data;
if (data == -1)
return NULL;
else
{
p = new TreeNode(data);
p->left = createTreeNode();
p->right = createTreeNode();
}
return p;
}
//主函數
int main() {
while (cin) {
TreeNode* p = createTreeNode();
cout << "遞歸先序遍歷:\t";
preProcess(p);
cout << endl;
cout << "Morris先序遍歷:";
preMorris(p);
cout << endl;
cout << "遞歸中序遍歷:\t";
inProcess(p);
cout << endl;
cout << "Morris中序遍歷:";
inMorris(p);
cout << endl;
cout << "遞歸後序遍歷:\t";
postProcess(p);
cout << endl;
cout << "Morris後序遍歷:";
postMorris(p);
cout << endl;
cout << "爲了保證那個鏈表逆轉來逆轉去恢復正確,再先序打印一遍看看:";
preProcess(p);
cout << endl;
}
}
測試結果:
總結:理解了Morris遍歷之後,許多利用二叉樹遍歷的題目都可以使用Morris解決的