Tree Traversals Again(根據先序中序建樹、不建樹也可解決)

原題鏈接

樹(上)課後練習題3

解題思路

這道題是給了用棧實現的中序遍歷的非遞歸中間過程,要求根據這個中間過程建樹並對其進行後序遍歷。那首先得理解用棧實現的非遞歸輸出的中間結果包含了什麼信息,先了解一下遞歸和非遞歸遍歷的各自實現方式。

遞歸的中序遍歷

沒啥說的,左子樹,根,右子樹的遞歸訪問。

// 遞歸寫法 
void inOrderTraversal(node* root){
	if(root){
		// printf("%d ", root->data); // 先序是在前面訪問
		inOrderTraversal(root->left);
		//printf("%d ", root->data); // 中序是在中間訪問 
		inOrderTraversal(root->right);
		//如果只需要輸出葉子結點
		//if(!root->left && !root->right) 
		printf("%d ", root->data); // 後序是在後面訪問 
	}
}

非遞歸的中序遍歷

覺得不好理解就再看看何老師的視頻

如何對一棵樹進行非遞歸的遍歷?基本思想就是利用

  1. 遇到一個結點,將其壓棧,並去遍歷它的左子樹
  2. 左子樹遍歷結束時,從棧頂彈出這個結點並訪問它
  3. 按其右指針再去遍歷它的右子樹
// 非遞歸寫法
void inTraversal(node* root){
	node* temp = root;
	stack<node*> s;
	while(temp || !s.empty()){
		while(temp){
			s.push(temp); // 第一次遇到該結點 
			//printf("%d ", temp->data); // 先序是第一次遇到時訪問 
			temp = temp->left; 
		}
		if(!s.empty()){
			temp = s.top(); // 第二次遇到該結點 
			printf("%d ", temp->data); // 中序是第二次遇到時訪問 
			s.pop();
			temp = temp->right; // 轉向右子樹 
		} 
	}
} 

在第一次遇到某個結點時就訪問它,其他過程不變,就很容易的得到了非遞歸的先序遍歷。

非遞歸的後序遍歷

這個不像改成先序遍歷那麼容易,也貼一下。
這裏的變化是因爲根結點最後訪問,所以如果沒有右孩子當然和之前一樣直接訪問根結點,那如果有右孩子的話就要考慮右子樹是否已經被訪問過了,訪問過了就和之前一樣直接訪問根結點,如果沒有的話就先去訪問該結點的右子樹,最後再來訪問該結點。

void postTraversal(node* root){
	node* temp = root;
	node* pre = NULL; //指向上一個訪問的結點 
	stack<node*> s;
	while(temp || !s.empty()){
		while(temp){
			s.push(temp);
			printf("push %d\n", temp->data);
			temp = temp->left; 
		}
		if(!s.empty()){
			temp = s.top();
			if(temp->right == NULL || temp->right == pre){
				// 如果沒有右孩子或者右孩子已經訪問好了 
				printf("null %d\n", temp->data);
				s.pop();
				pre = temp;
				temp = NULL; // 將當下指針指向空,不然會重新壓入棧。 
			}
			else{
				temp = temp->right;
			} 
		}
	}
} 

迴歸本題(有先序和中序如何建樹)

這其實是很常見的問題。
貼一下輸入樣例

6
Push 1
Push 2
Push 3
Pop
Pop
Push 4
Pop
Pop
Push 5
Push 6
Pop
Pop

再貼一下這棵樹的樣子
在這裏插入圖片描述
根據非遞歸的中序遍歷的實現方式可以知道(觀察也能觀察出來其實)Push的順序是先序遍歷的順序!
而Pop的順序是中序遍歷的順序。
有了先序和中序,就可以愉快的建樹了。

建樹時也是一個遞歸的過程:
首先我們得到中序和先序的數組,先序數組的下標範圍記爲preL、preR,中序數組的下標範圍記爲inL、inR。

  1. 在中序中找到先序的第一個元素(這是根),記錄其下標k
  2. 根據下標就可以計算出左子樹和右子樹的元素個數(也可以說是元素範圍),例如左子樹元素個數numLeft爲k-inL。
  3. 遞歸建立根的左子樹和右子樹。例如左子樹的先序數組範圍變爲了preL+1、preL+numLeft,中序數組下標範圍變爲了inL, k-1。

注意遞歸邊界:preL>preR。

這個我貼一下算法筆記中的圖解,非常好理解。
在這裏插入圖片描述
在這裏插入圖片描述
再看看代碼實現:

node* buildTree(int preL, int preR, int inL, int inR){
	if(preL > preR) return NULL;
	node* root = new node; //記得要新建空間啊!這裏老忘記,不new一個node就出錯了 
	root->data = pre[preL];
	int k = 0;//根節點在中序中的位置
	for(int i=inL; i<=inR; i++){
		if(in[i] == root->data){
			k = i;
			break;
		}
	} 
	int numLeft = k-inL;
	root->left = buildTree(preL+1, preL+numLeft, inL, k-1);
	root->right = buildTree(preL+numLeft+1, preR, k+1, inR);
	return root;
} 

方法二(不建樹)

更新… 今天看了陳越老師對這道題的講解,可以不建樹直接通過先序和中序寫出後序,也是遞歸過程。厲害誒,代碼裏可以少一半了。

這個直接寫的過程和上面寫的根據先序和中序建樹一模一樣其實,算法真正厲害的地方就是將思想簡潔的表明出來然後運用,而不是像我這樣傻傻的真的去建個實在的樹!

先序確定根,在中序中找到根的下標,確定左右子樹的個數,計算好範圍遞歸左右子樹。

void solve(int preL, int inL, int postL, int num){
	//通過先序和中序數組直接寫出後序數組
	if(num == 0) return;
	if(num == 1){
		post[postL] = pre[preL];
		return ;
	} 
	int root = pre[preL];//根結點是先序第一個
	post[postL+num-1] = root;
	//在中序中找根結點下標
	int k;
	for(int i=0; i<num; i++){
		if(in[inL+i] == root){
			k = i;
			break;
		}
	} 
	int L = k, R = num-L-1;//左右子樹的個數
	//遞歸處理 
	solve(preL+1, inL, postL, L); 
	solve(preL+L+1, inL+L+1, postL+L, R); 
} 

源代碼

#include<iostream>//03-樹3 
#include<cstring>
#include<stack> 
using namespace std;
const int maxn = 40;
int pre[maxn], in[maxn], post[maxn];
stack<int> s;
int n;
struct node{
	int data;
	node* left;
	node* right;
};
node* buildTree(int preL, int preR, int inL, int inR){
	if(preL > preR) return NULL;
	node* root = new node; //記得要新建空間啊! 
	root->data = pre[preL];
	int k = 0;//根節點在中序中的位置
	for(int i=inL; i<=inR; i++){
		if(in[i] == root->data){
			k = i;
			break;
		}
	} 
	int numLeft = k-inL;
	root->left = buildTree(preL+1, preL+numLeft, inL, k-1);
	root->right = buildTree(preL+numLeft+1, preR, k+1, inR);
	return root;
} 
int idx = 0;
void postOrderTraversal(node* root){
	if(root){
		postOrderTraversal(root->left);
		postOrderTraversal(root->right);
		post[idx++] = root->data;
	}
}
int main(){
	cin>>n;
	string str;
	int i = 0, j = 0, data; // 先序和中序的下標 
	int num = 2*n;
	for(int k=0; k<num; k++){
		cin>>str;
		if(str == "Push"){
			cin>>data;
			s.push(data);
			pre[i++] = data;
		}	
		else{
			in[j++] = s.top();
			s.pop();
		}
	}
	//方法一:根據先序和中序建樹,再進行後序遍歷 
	//node* root = buildTree(0, n-1, 0, n-1);
	//postOrderTraversal(root);
	
	//方法二:不建樹 
	solve(0, 0, 0, n);
	for(int i=0; i<n; i++){
		printf("%d", post[i]);
		if(i != n-1){
			printf(" ");
		}
	}
	return 0;
}

心得收穫

之前從來沒有考慮過如果對樹進行非遞歸的遍歷,但通過這門課明白了遞歸的本質就是棧。

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