前言
学习了如何利用前序遍历和中序遍历来重建二叉树,我就在思考是否知道了任意两种遍历方式,都能将二叉树重构出来呢?仔细研究了一晚,果然如此。
找规律
先看三种遍历方式
struct Tree{
int val;
Tree *left;
Tree *right;
};
void printTree(Tree *node,int mode){
if(node!=nullptr){
switch(mode){
case 1:
cout<<node->val<<",";
printTree(node->left, mode);
printTree(node->right, mode);
break;
case 2:
printTree(node->left, mode);
cout<<node->val<<",";
printTree(node->right, mode);
break;
case 3:
printTree(node->left, mode);
printTree(node->right, mode);
cout<<node->val<<",";
break;
default:
break;
}
}
}
代码不多说,都明白,1、2、3分别对应前、中、后序遍历。
首先最重要的一个规律就是,除去根结点的数据,每次调用函数都是先输出左子树的所有数据,然后再输出右子树的数据。这个规律与递归无关,相信应该很容易就能发现。
//cout<<node->val<<",";
printTree(node->left, mode);
//cout<<node->val<<",";
printTree(node->right, mode);
//cout<<node->val<<",";
再说第二个规律,前序遍历每次输出的第一个必然是根结点的数据,后序遍历则相反,每次输出的最后一个才是根结点的数据。
前序遍历与中序遍历
前序遍历:3,0,1,2,6,5,4,8,7,9
中序遍历:0,1,2,3,4,5,6,7,8,9
根据前序遍历的规律,我们可以知道根结点为3。通过根结点我们将中序遍历划分为左子树与右子树。
中序遍历:[0,1,2] [3] [4,5,6,7,8,9]
根结点:3
左子树:0,1,2
右子树:4,5,6,7,8,9
此时由于知道了左右子树的个数,可将前序遍历也划分为左子树与右子树
前序遍历:[3] [0,1,2] [6,5,4,8,7,9]
根结点:3
左子树:0,1,2
右子树:6,5,4,8,7,9
对新生成的左子树与右子树重复进行上面的操作,直到每一个节点都当过根结点。
来看实现代码:
//pre:前序遍历
//mid:中序遍历
//length:数组长度
Tree* Fun1(const int *pre, const int *mid,const int length) {
//当空指针或者数组长度为0时返回
if (pre == NULL || mid == NULL || length <= 0)
return NULL;
//建立根节点
Tree *head = new Tree();
head->value=pre[0];
head->left=head->right=NULL;
//找到中序遍历中根节点所在位置
int index = 0;
while (index<length) {
if (head->value == mid[index])
break;
++index;
}
//划分左子树
head->left = Fun1(pre + 1, mid, index);
//划分右子树
head->right = Fun1(pre + index +1, mid + index + 1, length - index - 1);
return head;
}
中序遍历与后序遍历
原理与上面基本相同,只不过根结点放到数组的最后,此处不给解释,希望各位能够自己琢磨。
//mid:中序遍历
//back:后序遍历
//length:数组长度
Tree* Fun2(const int *mid,const int *back,const int length){
//当空指针或者数组长度为0时返回
if(back==NULL||mid==NULL||length<=0)
return NULL;
//建立根节点
Tree *head=new Tree();
head->val=back[length-1];
head->left=head->right=NULL;
//找到中序遍历中根节点所在位置
int index=0;
while(index<length){
if(head->val==mid[index]){
break;
}
++index;
}
//划分左子树
head->left=Fun2(mid,back,index);
//划分右子树
head->right=Fun2(mid+index+1, back+index, length-index-1);
return head;
}
前序遍历与后序遍历
前序遍历:3,0,1,2,6,5,4,8,7,9
后序遍历:2,1,0,4,5,7,9,8,6,3
看过前面的推导,相信找到根结点数据3对各位来说一点难度都没有。难点在于如何把左子树和右子树分开。
仔细观察,可以发现有一个非常有意思的规律,前序遍历的第一位总是与后序遍历的最后一位相同(毕竟都是根结点数据)。
前序遍历:[3],0,1,2,6,5,4,8,7,9
后序遍历:2,1,0,4,5,7,9,8,6,[3]
把根结点数据去掉:
前序遍历(左子树+右子树):0,1,2,6,5,4,8,7,9
后序遍历(左子树+右子树):2,1,0,4,5,7,9,8,6
因为左子树在前,所以此时前序遍历的第一位必然是左子树的根结点,同时我们还知道后序遍历左子树的根结点在左子树数据的最后一位,咱们标注一下:
前序遍历(左子树+右子树):[0],1,2 + 6,5,4,8,7,9
后序遍历(左子树+右子树):2,1,[0] + 4,5,7,9,8,6
这样一来就可以轻松的将左子树和右子树区别开来了。然后再进行进一步划分。
这样就完了吗?当然没有,其实这里还有一个小小的陷阱,以刚刚我们区分出来的左子树为例子:
前序遍历:[0],1,2
后序遍历:2,1,[0]
根据我们上面的方法,可以知道此时子树的区分:
前序遍历(左子树+右子树):[1],2 + NULL
后序遍历(左子树+右子树):2,[1] + NULL
然而此时[1,2]并非左子树(1、2均大于根节点0),而是右子树。
这种情况是由于左右子树其中一个为空,此时需要进行简单的判断(小左大右)。
OK,可以上代码了:
//pre:前序遍历
//back:后序遍历
//length:数组长度
Tree* Fun3(const int *pre,const int *back,const int length){
//当空指针或者数组长度为0时返回
if(pre==NULL||back==NULL||length<=0)
return NULL;
//建立根节点
Tree *head = new Tree();
head->value=pre[0];
head->left=head->right=NULL;
//数组长度为1时,直接返回,因为没有子节点了
if(length>1){
//找到左右子树的分界点
int index=0;
while(index<length){
if(pre[1]==back[index]){
break;
}
++index;
}
//判断是否是唯一子树
if(index==length-2){
//唯一子树需要判断为左子树还是右子树
if(back[0]<head->val){
head->left=Fun3(pre+1,back,index+1);
}else{
head->right=Fun3(pre+1, back, index+1);
}
}else{
//左右子树均存在时,左边的为左子树,右边的为右子树
head->left=Fun3(pre+1,back,index+1);
head->right=Fun3(pre+index+2, back+index+1, length-index-2);
}
}
return head;
}