Morris遍歷

一、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解決的 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章