中序遍历和先序遍历/后序遍历构建二叉树

数据结构与算法 专栏收录该内容
2 篇文章 0 订阅

1、问题

给定二叉树的2个遍历序列(如先序+中序,先序+后序,中序+后序等),是否能够根据这2个遍历序列唯一确定二叉树?


2、理论分析

数据结构的基础知识中重要的一点就是能否根据两种不同遍历序列的组合(有三种:先序+中序,先序+后序,中序+后序),唯一的确定一棵二叉树。然后就是根据二叉树的不同遍历序列(先序、中序、后序),重构二叉树。显然,这三种组合并不是都能唯一确定二叉树的,其中先序+后序就不能唯一确定一棵二叉树,下面是关于该问题的证明与结论。

给定二叉树结点的前序序列和中序序列,可以唯一确定该二叉树
证明:因为先序序列的第一个元素是根结点,该元素将二叉树中序序列分成两部分,左边(假设有L个元素)表示左子树,若左边无元素,则说明左子树为空;右边(假设有R个元素)是右子树,若为空,则右子树为空。根据前序遍历中"根-左子树-右子树"
的顺序,则由从先序序列的第二元素开始的L个结点序列和中序序列根左边的L个结点序列构造左子树,由先序序列最后R个元素序列与中序序列根右边的R个元素序列构造右子树。

②由中序序列和先序序列能唯一确定一棵二叉树,但是由先序序列和后序序列不能唯一确定一棵二叉树,因无法确定左右子树两部分。
反例:任何结点只有左子树的二叉树和任何结点只有右子树的二叉树,其前序序列相同,后序序列相同,但却是两棵不同的二叉树。
如: 2           2
    /            \
   1              1 
  /                \
 3                  3
这两棵二叉树的先序遍历序列都为2-1-3,后序遍历序列都为3-1-2。但是显然它们是不同的二叉树,所以根据先序序列和后序序列并不能唯一确定二叉树。

③已经说明由二叉树的先序序列和中序序列可以确定一棵二叉树,现在来证明由二叉树的中序序列和后序序列,也可以唯一确定一棵二叉树。

证明:
当n=
1时,只有一个根结点,由中序序列和后序序列可以确定这棵二叉树。
设当n=m-1时结论成立,即结点数目为m-1时,中序序列和后序序列可以唯一确定二叉树。现证明当n=
m时结论成立。
设中序序列为S1,S2,…,Sm,后序序列是P1,P2,…,Pm。因后序序列最后一个元素Pm是根,则在中序序列中可找到与Pm相等的结点(设二叉树中各结点互不相同)Si(1≤i≤m),因中序序列是由中序遍历而得,所以Si是根结点,S1,S2,…,Si-1是左子树的中序序列,而Si+1,Si+2
,…,Sm是右子树的中序序列。
若i=1,则S1是根,这时二叉树的左子树为空,右子树的结点数是m-1,则{S2,S3,…,Sm}和{P1,P2,…,Pm-1
}可以唯一确定右子树,从而也确定了二叉树。
若i=m,则Sm是根,这时二叉树的右子树为空,左子树的结点数是m-1,则{S1,S2,…,Sm-1}和{P1,P2,…,Pm-1
}唯一确定左子树,从而也确定了二叉树。
最后,当1<i<m时,Si把中序序列分成{S1,S2,…,Si-1}和{Si+1,Si+2,…,Sm}。由于后序遍历是"左子树-右子树-根结点",所以{P1,P2,…,Pi-1}和{Pi,Pi+1,…Pm-1}是二叉树的左子树和右子树的后序遍历序列。因而由{S1,S2,…,Si-1}和{P1,P2,…,Pi-1}可唯一确定二叉树的左子树,由{Si+1,Si+2,…,Sm}和{Pi,Pi+1,…,Pm-1}可唯一确定二叉树的右子树。


3、构造思路


1)根据先序遍历序列和中序遍历序列构建二叉树

假定已知二叉树如下:

        ___7___
       /     \
    10        2
   /   \      /
  4    3      8
        \    /
         1  11
那么它的先序遍历和中序遍历的结果如下:

preorder = {7,10,4,3,1,2,8,11}
inorder = {4,10,3,1,7,11,8,2}

需要关注几个重要的点:

1)先序遍历的第一个结点总是根结点。如上图中的二叉树,根结点为7,也是先序遍历的第一个值。先序遍历时父亲结点总是在孩子结点之前遍历。

2)可以观察到在中序遍历中,7是第4个值(从0开始算起)。由于中序遍历顺序为:左子树,根结点,右子树。所以7左边的{4,10,3,1} 这四个结点属于左子树,而根结点7右边的{11,8,2}属于右子树。

3)可以从上面的结论很轻松的得到递归式。在构建了根结点7后,我们可以根据中序遍历{41031} 和{11,8,2}分别构建它的左子树和右子树。我们同时需要相应的先序遍历结果用于发现规律。我们可以由先序遍历知道左右子树的先序遍历分别是{10,4, 3, 1}和{2, 8, 11}。左右子树也分别为二叉树,由此可以递归来解决问题。

4)关于如何得到根结点在中序遍历中的位置问题还没有细讲,如果使用线性扫描查找位置,则每次查找需要O(N)的时间,如果二叉树平衡的话,则整个算法需要O(NlgN)的时间。如果二叉树不平衡,则最坏情况需要O(N^2)时间。为了提高效率,我们可以考虑使用哈希表来存储与查找根结点在中序遍历中的位置,每次查找只需要O(1)的时间,这样构建整棵树只需要O(N)的时间。 这里为了方便,只是用了一个数组用于标记位置,要是用哈希表也会很方便。需要注意的是,这里的二叉树结点值不能有相同的值。

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int mapIndex[256];  
  2. void mapToIndices(int inorder[], int n)  
  3. {  
  4.     int i;  
  5.     for (i=0; i<n; i++) {  
  6.         mapIndex[inorder[i]] = i;  
  7.     }  
  8. }  
  9. //在这之前要调用mapToIndices方法。pre数组为先序遍历序列,注意在递归过程中pre起始位置是变化的。n为结点数目,offset为子树开始位置。  
  10. struct node* buildInorderPreorder(int pre[],  
  11.         int n, int offset)  
  12. {  
  13.     if (n == 0) return NULL;  
  14.     int rootVal = pre[0];  
  15.     int i = mapIndex[rootVal] - offset;  
  16.     struct node* root = newNode(rootVal);  
  17.     root->left = buildInorderPreorder(pre+1,  
  18.             i, offset);  
  19.     root->right = buildInorderPreorder(pre+i+1,  
  20.             n-i-1, offset+i+1);  
  21.     return root;  
  22. }  
  23. //测试代码  
  24. void buildInorderPreorderTest()  
  25. {  
  26.     int pre[] = {7, 10, 4, 3, 1, 2, 8, 11};  
  27.     int in[] = {4, 10, 3, 1, 7, 11, 8, 2};  
  28.     int n = sizeof(in) / sizeof(in[0]);  
  29.     int offset = 0;  
  30.     mapToIndices(in, n);  
  31.     struct node* root = buildInorderPreorder(pre, n, offset);  
  32.     traverse(root);  
  33.     putchar('\n');  
  34. }  


2)根据后序遍历和中序遍历构建二叉树

跟前面原理类似,代码如下:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. struct node *buildInorderPostorder(int post[], int n, int offset)  
  2.  {  
  3.   assert(n >= 0);  
  4.   if (n == 0) return NULL;  
  5.   int rootVal = post[n-1];  
  6.   int i = mapIndex[rootVal]-offset;  // the divider's index  
  7.   struct node *root = newNode(rootVal);  
  8.   root->left = buildInorderPostorder(post, i, offset);  
  9.   root->right = buildInorderPostorder(post+i, n-i-1, offset+i+1);  
  10.   return root;  
  11. }  
  • 1
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值