通过二叉树先、中、后序遍历,重构二叉树

专栏: 学习笔记
不久前刷蓝桥杯每日一题,在递归这一专栏刷到了”通过给定二叉树后序遍历和中序遍历,请你输出它的层序遍历“,刷完后,好奇心又驱使我在想这三者两两结合是不是都可以构成一个唯一的二叉树,经过查阅资料,我发现【先序+后序】不能构成唯一的二叉树(不包含特殊情况,下文会介绍),而【先序+中序】或【中序+后序】可以构成唯一的二叉树。

在解释上述语句和实现代码前,我们先回顾一下什么是先、中、后序遍历~

先、中、后序遍历

先序遍历

先访问根节点,再遍历左子树,最后遍历右子树。

遍历结果:5 2134 8697

中序遍历

先遍历左子树,再访问根节点,最后遍历右子树。

遍历结果:1243 5 6987

后序遍历

先遍历左子树,再遍历右子树,最后访问根节点。

遍历结果:1432 9678 5

代码实现它们请参考:二叉树前序、中序、后序三种遍历(C++)

最后还有个层序遍历:其实就是从上到下,从左到右依次遍历二叉树。

遍历结果:5 28 1367 49

自己画的,将就看下ʕ•͡-•ʔ。

解释开头语句

【先序+中序】和【中序+后序】都含有一个中序,而【先序+后序】就不含中序。由此可见中序对于确定唯一的二叉树是至关重要的,那原因是什么呢?

先序:根,左,右
后序:左,右,根
你可以发现这样根本无法把”左,右“节点分开确定。
而有了中序遍历:左,根,右。你可以通过先序或后序的根节点把它的”左“和”右“ 分开,这样每次递归处理后序(或先序)的左边和右边【中序的左,右子树长度确定后,后序(或先序)自然就确定了】,就能找到对应根节点的左孩子和右孩子。
开头那个【先序+中序】还有特殊情况?what ? why ?
下面根据两张图片对比说明~
你可以看出该二叉树如果某个节点只有一个孩子节点时,是不能唯一确定一个二叉树的。
相反,如果该二叉树每个节点存在左右孩子节点或不存在左右孩子节点时,这个特殊情况就能唯一的确定一个二叉树。
☆不能唯一确定原因:比如说图一,它的先序遍历:4635 后序遍历:3654
构造出来的树可以是:图一3是6的左子树,也可能3是6的右子树,这是通过先序和后序是不能确定的。
☆可以唯一确定原因:比如说图二,它的先序遍历:46532 后序遍历:63254
这就奇怪了,你不是上文说根据【先序+后序】不能将”左,右“节点分开确定嘛,这怎么又可以唯一确定了。我帮大家查了一下。
大概原因:我们先取 先序的根节点4 再取其下一个节点6,将其视为根节点,然后对应的在后序遍历中找到这个6的位置,因为后序是:左,右,根。所以后序里的6前面对应的就是自己的左孩子和右孩子 那么后序中6和4之间的节点就是4节点的孩子,当然6也一定是4的孩子节点( 如果后序6和4之间有节点,那么6一定为4的左节点,否则,无法确定6的位置,因此这也是不能唯一确定的原因 )。
大概思想:我探讨的前提是建立在这个特殊情况之上,思想其实和【含中序的方法】差不太多,因为我们 关键就是 以先序遍历的根节点后一个节点为根节点去后序遍历中查找其左右子树 。我们每次递归处理根节点的左子树和右子树就可以了(一开始pref、prel:先序第一个位置和最后一个位置 ;posf、posl:后序........;
build_left(pref+1,pref+1+num,posf,k);//pref+1表示pref的左孩子。
//k 表示pref+1这个节点在后序遍历中的位置
//num 表示pref+1这个节点的左右孩子个数
//这里的[ posf,k ]区间的序列都是pref的左孩子
build_right(pref+num+2,prel,k+1,posl-1); //同理右孩子也是同样的道理

完整代码我后序可能会更新~

【中序+后序】

一个二叉树,树中每个节点的权值互不相同。
现在给出它的后序遍历和中序遍历,请你输出它的层序遍历。
输入格式
第一行包含整数 N,表示二叉树的节点数。
第二行包含 N 个整数,表示二叉树的后序遍历。
第三行包含 N 个整数,表示二叉树的中序遍历。
输出格式
输出一行 N 个整数,表示二叉树的层序遍历。
数据范围
1≤N≤30,
官方并未给出各节点权值的取值范围,为方便起见,在本网站范围取为 1∼N。
输入样例:
7
2 3 1 5 7 6 4
1 2 3 4 5 6 7
输出样例:
4 1 6 3 5 7 2

这种递归类的题,重点是理清思路和递归的构建,不要去死死的分析递归的整个过程,那是相当痛苦的,我们只需要把递归式写出来就行了,剩下的复杂递归过程交给计算机就行了୧꒰•̀ᴗ•́꒱୨

此题关键是:将后序遍历的左子树或右子树最后一个节点(即下文ar)作为根节点,然后递归对应的左右子树

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;
const int N = 35;
int n;
int a[N], b[N], p[N];
vector<int> level[N];//把每一层的节点存下来

void build(int al, int ar, int bl, int br, int d)//al,ar分别表示在后续遍历的起点和终点。d表示层数
{
    if (al > ar) return;//区间为空,说明已经递归到最后一个子树直接返回
    int val = a[ar];//将后序遍历的左子树或右子树最后一个节点作为根节点
    level[d].push_back(val);//把当前层的根节点存入level,d层里
    int k = p[val];//根节点在中序遍历里面的位置
    build(al, al + k - 1 - bl, bl, k - 1, d + 1);//先递归左子树,层数加一。
    build(al + k - bl, ar - 1, k + 1, br, d + 1);//然后递归右子树,左子树后续遍历的下一个位置,起点是上面终点加一,最后结尾记得减一
}

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++ ) cin >> a[i];//a代表后序遍历
    for (int i = 0; i < n; i ++ ) cin >> b[i];//b代表中序遍历
    for (int i = 0; i < n; i ++ ) p[b[i]] = i;//p用来存在中序遍历中每一个数值的位置,这里用unordered_map也可以,只是数组存更快
    build(0, n - 1, 0, n - 1, 0);//构建子树

    for (int i = 0; i < n; i ++ )
        for (int x: level[i]) //把每一层从左到右依次输出
            cout << x << ' ';

    return 0;
}
build(al, al + k - 1 - bl, bl, k - 1, d + 1);//先递归左子树,层数加一。
build(al + k - bl, ar - 1, k + 1, br, d + 1);//然后递归右子树,左子树后续遍历的下一个位置,起点是上面终点加一,最后结尾记得减一
这句话如何分析呢?
上文不是说了,中序遍历:左,根,右。你可以通过后序的根节点把中序的”左“和”右“ 分开,然后对应的就可以知道后序遍历的左右子树长度
递归左子树时,后序的左子树的终点x满足:x-a1=k-1-bl <=>x=al+k-1-bl
中序的左子树的终点就是k-1
递归右子树时,后序的右子树的起点x就是x+1,终点记的减1
中序的右子树的起点就是k+1

⚠️注意:整个递归过程应该是左边那个图,先递归完1步骤,在返回到开始,递归2步骤,不是右边那个图按着顺序递归左,右子树的。

【先序+中序】

思想和【后序+中序】是一样的,关键是:将先序遍历的左子树或右子树的第一个节点(即下文的al )作为根节点,然后递归对应的左右子树~

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 35;

int n;
int a[N], b[N], p[N];
vector<int> level[N];//把每一层的节点存下来

void build(int al, int ar, int bl, int br, int d)//al,ar分别表示先续遍历的起点和终点。d表示层数
{
    if (al > ar) return;//如果说区间为空,直接返回
    int val = a[al];//每次递归,以先序左子树或右子树第一个节点作为根节点
    level[d].push_back(val);//把当前层的根节点存入level,d层里
    int k = p[val];//根节点在中序遍历里面的下标
    build(al+1, al + k - bl, bl, k - 1, d + 1);//先遍历左子树,层数加一。
    build(al + k - bl + 1, ar , k + 1 , br, d + 1);//然后遍历右子树,左子树先续遍历的下一个位置,起点是上面终点加一,最后结尾记得减一
}

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++ ) cin >> a[i];//a代表先序遍历
    for (int i = 0; i < n; i ++ ) cin >> b[i];//b代表中序遍历
    for (int i = 0; i < n; i ++ ) p[b[i]] = i;//p用来存在中序遍历中每一个数值的位置,这里用unordered_map也可以,只是数组存更快
    build(0, n - 1, 0, n - 1, 0);//构建子树

    for (int i = 0; i < n; i ++ )
        for (int x: level[i]) //把每一层从左到右依次输出
            cout << x << ' ';

    return 0;
}

上述代码其实就是build不同,其他几乎完全一样,没什么可讲的了。

结束语

我写上述内容主要是为了满足自己的好奇心,当然肯定有很多不详细或者有错误的地方,请各位佬一定要指出了,还有我查资料,发现这部分题解几乎全是【结构体+指针+递归】的方法写的,主要是我还不太熟悉结构体+指针,所以就马马虎虎的用数组+结构体的方式基于上述题目写了解题方法,当然我一定尽快把结构体和指针掌握,然后再用【结构体+指针+递归】的方法题解,写一遍,相信我一定会再来更新这篇文章的。

References

[1] 二叉树前序、中序、后序三种遍历(C++)

[2] 关于二叉树先序遍历和后序遍历为什么不能唯一确定一个二叉树分析

  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吹往北方的风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值