原題鏈接
解題思路
這道題是給了用棧實現的中序遍歷的非遞歸中間過程,要求根據這個中間過程建樹並對其進行後序遍歷。那首先得理解用棧實現的非遞歸輸出的中間結果包含了什麼信息,先了解一下遞歸和非遞歸遍歷的各自實現方式。
遞歸的中序遍歷
沒啥說的,左子樹,根,右子樹的遞歸訪問。
// 遞歸寫法
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); // 後序是在後面訪問
}
}
非遞歸的中序遍歷
如何對一棵樹進行非遞歸的遍歷?基本思想就是利用棧。
- 遇到一個結點,將其壓棧,並去遍歷它的左子樹
- 當左子樹遍歷結束時,從棧頂彈出這個結點並訪問它
- 按其右指針再去遍歷它的右子樹
// 非遞歸寫法
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。
- 在中序中找到先序的第一個元素(這是根),記錄其下標k
- 根據下標就可以計算出左子樹和右子樹的元素個數(也可以說是元素範圍),例如左子樹元素個數numLeft爲k-inL。
- 遞歸建立根的左子樹和右子樹。例如左子樹的先序數組範圍變爲了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;
}
心得收穫
之前從來沒有考慮過如果對樹進行非遞歸的遍歷,但通過這門課明白了遞歸的本質就是棧。