leetcode115.从中序与后序遍历序列构造二叉树,手把手带你构造二叉树(新手向)

构造二叉树是树问题中的难点(相对于遍历二叉树),一开始做的读者会感觉无从下手,这道题在训练营专栏里讲过,是四道题一起讲的,但是现在看来讲的并不全面、具体,所以想单独出一期再来讲一下如何构造二叉树。

这道题给我们中序和后序遍历数组,首先要知道怎么使用它们,后序遍历的特点是左右中的顺序去遍历一棵二叉树,换句话说遍历二叉树总是最后的遍历中间节点,根据这个特性我们可以知道每次要处理的中间节点实际上就在每次递归遍历中的后序数组的最后一个位置上。

举例说明:

先不讲代码,先看中序数组和后序数组如何创建二叉树

inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]

从之前说的我们知道root节点就是3,以3这个节点去切割中序遍历的数组,把中序数组分成左子树和右子树部分,左子树部分是9,右子树部分是15、20、7,删除后序遍历数组最后一个数字,然后进行下一层递归,取后序最后一个数字,20作为中序数组分割条件,这一次把数组分成15和7两部分,至此分割完成。

也就是leetcode图中给出的这个示例图。

仔细对照我们说的逻辑,看看能否分成如上图的树,若能理解,请看下面讲解。

写代码之前将代码逻辑讲清楚,代码书写逻辑分为以下七步:

第一步递归判断若后序数组此时无数据,直接返回NULL。

第二步取当前后序遍历数组最后一个数字,并创建一个节点。

第三步判断当前后序遍历数组长度是否为1,若为1说明该节点是叶子节点,不需要向下再递归,直接返回当前节点指针,特别注意,有读者可能觉得这里有些奇怪,第一步是否有些多余?既然节点为叶子节点直接返回,那怎么可能走到数组为空再返回呢?这个我们后面会单独讲解区别。

第四步以当前创建出的节点的值为切割点,在中序遍历数组找到对应切割点位置,并进行切割

第五步切割后序遍历数组

第六步将当前节点左指针对应递归中序遍历数组左子树和后序遍历数组左子树,右指针对应递归中序遍历数组右子树和后序遍历数组右子树。

第七步走到最后说明树创建完成,返回节点。

或许这其中某一步或者某些步看起来有些难以理解,但是我会在之后用代码的形式解释

第一步代码就是简单的判断

if(postorder.size()==0)return NULL;

第二步存储当前后序遍历数组最后一个数字并创建新节点

        int val=postorder[postorder.size()-1];
        TreeNode* node=new TreeNode(val);

第三步判断当前后序数组长度是否为1

if(postorder.size()==1)return node;

第四步——找到切割点位置在中序数组中

        int index=0;
        for(;index<inorder.size();++index){
            if(inorder[index]==val)break;
        }

 第四步——切割中序数组

        vector<int>leftinorder(inorder.begin(),inorder.begin()+index);
        vector<int>rightinorder(inorder.begin()+index+1,inorder.end());

 第五步切割后序数组(不要忘记先删除最后的一个节点)

    postorder.pop_back();
    vector<int>leftpostorder(postorder.begin(),postorder.begin()+leftinorder.size());
    vector<int>rightpostorder(postorder.begin()+leftinorder.size(),postorder.end());

 第六步将当前节点左指针对应递归中序遍历数组左子树和后序遍历数组左子树,右指针对应递归中序遍历数组右子树和后序遍历数组右子树。

        node->left=back(leftinorder,leftpostorder);
        node->right=back(rightinorder,rightpostorder);

第七步返回

        return node;

看完整代码

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* back(vector<int>& inorder,vector<int>&postorder){
        if(postorder.size()==0)return NULL;
        int val=postorder[postorder.size()-1];
        TreeNode* node=new TreeNode(val);
        if(postorder.size()==1)return node;
        int index=0;
        for(;index<inorder.size();++index){
            if(inorder[index]==val)break;
        }
        vector<int>leftinorder(inorder.begin(),inorder.begin()+index);
        vector<int>rightinorder(inorder.begin()+index+1,inorder.end());
        postorder.pop_back();
    vector<int>leftpostorder(postorder.begin(),postorder.begin()+leftinorder.size());
    vector<int>rightpostorder(postorder.begin()+leftinorder.size(),postorder.end());
        node->left=back(leftinorder,leftpostorder);
        node->right=back(rightinorder,rightpostorder);
        return node;
    }
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        return back(inorder,postorder);   
    }
};

怎么样是不是特别清晰了,有注意到写代码时候有几步被特别标记在块引用里吗?这是代码中的重点部分。

现在由我来解答读者可能遇到的各种问题。.

问题1:为什么要先进行中序遍历数组的分割而不是后序的分割?

这个中序遍历数组分割我加粗很多次了,读者们应该都注意到了,这是为什么?实际上只能先进行中序数组分割,因为我们只能靠后序遍历数组来确定中间节点填写什么,中序数组无法确定节点,而后序数组无法确定左右子树分割,因为后序数组左右子树是挨在一起的,它没有分界点,这都是由于中序和后序的遍历顺序决定的,中序为左中右,后序为左右中,这也就是为什么中序能被中节点所分割的原因,而找中节点为什么要用后序数组的原因。

有一道很相似的题目是给你前序和中序遍历数组,让你构造一棵树,也是同样的道理,前序遍历由于先遍历中节点所以第一个位置的前序遍历数据就是中节点,拿这个数据去分割中序遍历数组就可以了,前序数组和后序数组不能构造二叉树,因为两种遍历方法均只能确定中节点,它们任何一个都无法对左右子树做分割,这是其中一个原因,另一个原因在于前后序遍历无法唯一确定一棵二叉树,这是最重要的原因。

我上面画的图它们是两颗完全不同的树,但是其前后序遍历数组内容是一样的。

问题2:我们上面说了后序遍历数组的左右子树部分数据是挨在一起的,不能用中节点分割,那我们是如何实现后序遍历数组的分割的?

这个和先分割的中序数组有关,我们用中序数组的左子树大小来分后序数组的左子树大小,中序数组的右子树大小来分后序数组的右子树大小,因为左右子树无论是怎么遍历节点数量肯定是不发生改变的,而中序遍历切割就是拿后序数组的数据切割的,所以中序的切割左右子树大小理应对应着后序遍历数组的左右子树大小,但是要注意中序左右子树的大小和后序左右子树大小相等,但是并不意味着其中左右子树的节点顺序也一定保持相同,因为两种遍历顺序的差异,根本不可能相同。还拿这个测试用例

inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]

读者自行举例便知结论的正确与否。

问题3:为什么切割中序遍历时右子树其实部分要用左子树的末尾下标+1?根据cpp的原理来看使用迭代器做这种结尾通常是不被包含的,那为什么右子树开始还要进行+1,这不会导致有一个节点被落下吗?

不要忘记我们为什么后序遍历数组要进行最后一个数据的删除,因为该中间节点已经被创建并加入了树的构建,所以一定不要使节点在下一次递归时重复出现。

问题4:步骤1和步骤3的思想是否发生重复?

这个是不重复的,而且没有步骤1会发生异常终止,但是没有步骤3没有问题,步骤3只是为了剪枝。遇到叶子节点就会向上返回所以不需要判断后序数组长度是否为0是错误的想法,该测试用例中使用中序【2,1】后序【2,1】的测试用例。
第一次递归分中后序数组的左右部分之后,后序数组由于减少一个元素的缘故,后序数组的右部分子树数组并没有数据,所以没有了第一个if判断,递归下去时候,会对一个无数据的数组取数,导致异常退出。整个过程中第二个if也就是叶子节点处返回都还没有进行,就导致异常终止了。
可见叶子节点及时返回这个终止逻辑并不能阻止此类bug的发生。

应该没有什么其他的问题了,如果读者有请在评论区留言给我。

使用vector在函数内部,每一次递归都需要开辟,为了节省空间和提高运行效率也可以用传入下标的方法来实现对递归时左右子树的控制,详情见下面代码。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* back(vector<int>&inorder,int inbegin,int inend,vector<int>&postorder,int pobegin,int poend){
        if(pobegin==poend)return NULL;
        int val=postorder[poend-1];
        TreeNode* root=new TreeNode(val);
        if(poend-pobegin==1)return root;
        int index;
        for(index=inbegin;index<inend;++index){
            if(inorder[index]==val)break;
        }
        int leftinorderbegin=inbegin,leftinorderend=index;
        int rightinorderbegin=index+1,rightinorderend=inend;
        int leftpostorderbegin=pobegin,leftpostorderend=pobegin+index-inbegin;
        int rightpostorderbegin=leftpostorderend;
        int rightpostorderend=poend-1;
        root->left=back(inorder,leftinorderbegin,leftinorderend,postorder,leftpostorderbegin,leftpostorderend);
        root->right=back(inorder,rightinorderbegin,rightinorderend,postorder,rightpostorderbegin,rightpostorderend);
        return root;
    }
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        return back(inorder,0,inorder.size(),postorder,0,postorder.size());
    }
};

代码思路和前面的代码思路一模一样,只是我们采用了下标传入的方法,核心思想是不变的,大家可以对照前次代码类比一下。


本期内容就到这里
如果对您有用的话别忘了一键三连哦,如果是互粉回访我也会做的!

大家有什么想看的题解,或者想看的算法专栏、数据结构专栏,可以去看看往期的文章,有想看的新题目或者专栏也可以评论区写出来,讨论一番,本账号将持续更新。
期待您的关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/219759.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

友菜友饭携手分众传媒,打造私厨到家生活新风尚

友菜友饭携手分众传媒 11月29日&#xff0c;友菜友饭与分众传媒签署战略合作协议&#xff0c;在全国重点城市全面引爆品牌力&#xff0c;携手打造全国领先的互联网数字化私厨平台&#xff0c;为中国5亿城市家庭解锁私厨到家服务新体验。 友菜友饭是全国领先的私厨到家平台&…

元宇宙红色展厅VR虚拟展馆提高受训者的参与感

生活在和平年代的新一代青少年&#xff0c;可能对革命先烈英勇事迹难以有很深的体会&#xff0c;无法切实感受到中国共产党无畏牺牲、誓死保家卫国的红色精神&#xff0c;因此借助VR虚拟现实制作技术&#xff0c;让参观者们走近革命先烈中&#xff0c;感受老一辈无产阶级革命家…

三、DVP摄像头调试笔记(图片成像质量微调整,非ISP)

说明&#xff1a;当前调试仅仅用来测试和熟悉部分摄像头寄存器模式 一、图片成像方向控制&#xff0c;基本每个摄像头都会有上下左右翻转寄存器 正向图片 反向图片 二、设置成像数据成各种颜色&#xff0c;&#xff08;黑白/原彩/黄色等等&#xff09; 在寄存器书册描述中…

低代码/无代码火热的缘由

目录 一、如何解决这个问题&#xff1f; &#xff08;1&#xff09;概念 &#xff08;2&#xff09;技术与产品 二、低代码究竟有没有用&#xff1f; 1.轻松解决企业复杂业务流程 2.强大接口引擎打破数据孤岛 3.最大限度满足企业个性化需求 4.有效把握控制开发效率成本 三、结语…

Hadoop学习笔记(HDP)-Part.17 安装Spark2

目录 Part.01 关于HDP Part.02 核心组件原理 Part.03 资源规划 Part.04 基础环境配置 Part.05 Yum源配置 Part.06 安装OracleJDK Part.07 安装MySQL Part.08 部署Ambari集群 Part.09 安装OpenLDAP Part.10 创建集群 Part.11 安装Kerberos Part.12 安装HDFS Part.13 安装Ranger …

图数据库知识点9 | 大数据框架与图数据架构异同

开门见山&#xff0c;直奔主题&#xff0c;接续前面的知识点&#xff1a; 【图数据库知识点1|图数据库与关系型数据库的区别&#xff1f;】 【图数据库知识点2 | 图思维方式】 【图数据库知识点3 | 图数据库解决了什么问题&#xff1f;】 【图数据库知识点4 | 图计算与图数…

Java基础数据类型

Java有八种基础的数据类型&#xff0c;它们被分为两个主要的类别&#xff1a;原始类型和引用类型。原始类型又被分为四类&#xff1a;整型、浮点型、字符型和布尔型。 整型&#xff08;Integral Types&#xff09;&#xff1a; 这些类型用于存储整数。它们包括&#xff1a; ○…

Python 数据清洗库详解

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 数据清洗是数据处理过程中至关重要的一部分。Python拥有许多强大的库&#xff0c;用于数据清洗和预处理&#xff0c;使得数据分析人员能够有效处理、转换和清洗数据。本文将介绍几个最常用的Python库&#xff0c…

git常用命令指南

目录 一、基本命令 1、创建分支 2、切换分支 3、合并分支 4、初始化空git仓库 二、文件操作 1、创建文件 2、添加多个文件 3、查看项目的当前状态 4、修改文件 5、删除文件 6、提交项目 三、实际操作 1、创建目录 2、进入新目录 3、初始化空git仓库 4、创建文…

【android开发-15】android中广播broadcast用法详解

1&#xff0c;broadcast类型 在Android中&#xff0c;Broadcast是一种用于在应用程序组件之间传递消息的机制。它允许一个组件&#xff08;发送者&#xff09;将消息发送给其他组件&#xff08;接收者&#xff09;&#xff0c;即使它们之间不存在直接的联系。 Android中的Bro…

耦合与内聚:软件设计中的黄金平衡

目录 1. 耦合&#xff08;Coupling&#xff09;的本质 1.1 强耦合与弱耦合 2. 内聚&#xff08;Cohesion&#xff09;的价值 2.1 任务内聚与数据内聚 3. 耦合与内聚的平衡 3.1 黄金平衡的追求 3.2 设计原则与模式的应用 4. 实际案例分析 5. 总结与展望 在软件设计的世界…

深入理解Java核心技术:Java工程师的实用干货笔记

&#x1f482; 个人网站:【 海拥】【神级代码资源网站】【办公神器】&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交流的小伙伴&#xff0c;请点击【全栈技术交流群】 在Java工程师的职业生涯中&#xff0c;深入理解…

项目中枚举的进阶用法(携带Java原理分析)

目录 1 枚举的普通用法1.1 无参1.2 单个参数1.3 两个参数 2 枚举的进阶用法&#xff08;核心&#xff09;2.1 优化2.1.1 需要改造的代码2.1.2 直接使用泛型2.1.3 使用反射---Class2.1.4 反射泛型 2.2 最终效果2.3 思考&#xff1a;类型擦除 遇到项目中这样一种写法&#xff0c;…

2023五岳杯量子计算挑战赛A题B题C题思路+模型+代码+论文

赛题思路&#xff1a;12月6日晚开赛后第一时间更新&#xff0c;获取见文末名片 “五岳杯”量子计算挑战赛&#xff0c;是国内专业的量子计算大赛&#xff0c;也是玻色量子首次联合移动云、南方科技大学共同发起的一场“企校联名”的国际竞赛&#xff0c;旨在深度融合“量子计算…

第二节JavaScript 语法、语句、注释、变量、数据类型等

一、JavaScript语法 1、JavaScript字面量 数字&#xff08;Number&#xff09;字面量&#xff1a;可以是整数或者是小数、或者是科学计数。 如&#xff1a;3.14 、1001 、123e5 字符串&#xff08;String&#xff09;字面量&#xff1a;可以使用单引号或双引号。 例如&…

2023年文章生成器推荐

2023年即将结束&#xff0c;今年可以说是大语言模型独领风骚的一年&#xff0c;对于内容创作来说&#xff0c;文章生成类的工具也发生了变化。今天给大伙介绍一些超赞的免费文章生成器&#xff0c;让你在内容创作的路上事半功倍。有了这些神奇的工具&#xff0c;你将能够轻松应…

从 MQTT、InfluxDB 将数据无缝接入 TDengine,接入功能与 Logstash 类似

利用 TDengine Enterprise 和 TDengine Cloud 的数据接入功能&#xff0c;我们现在能够将 MQTT、InfluxDB 中的数据通过规则无缝转换至 TDengine 中&#xff0c;在降低成本的同时&#xff0c;也为用户的数据转换工作提供了极大的便捷性。由于该功能在实现及使用上与 Logstash 类…

库函数qsort的使用及利用冒泡排序模拟实现qsort

文章目录 &#x1f680;前言&#x1f680;void*类型指针&#x1f680;库函数qsort的使用&#x1f680;利用冒泡排序实现库函数qsort() &#x1f680;前言 今天阿辉将为大家介绍库函数qsort的使用&#xff0c;还包括利用冒泡排序模拟实现qsort以及void*类型的指针&#xff0c;关…

【发布小程序配置服务器域名,不配置发布之后访问就会报错request:fail url not in domain list】

小程序在本地开发的时候大家通常会在微信开发者工具中设置“不校验合法域名、web-view (业务域名)、TLS 版本以及HTTPS证书”&#xff0c;久而久之可能会忘掉这个操作&#xff0c;然后打包直接上线发布&#xff0c;结果发现访问会报错request:fail url not in domain list&…

3d家居产品虚拟三维展示提升企业的品牌竞争力

2D展示逐渐难以满足消费者需求&#xff0c;因此基于3D三维展示制作平台将产品或服务以三维形式呈现的3D三维展示更受客户和企业青睐&#xff0c;也大幅提升企业的营销推广效果。那么3D三维展示制作平台如何赋能企业营销推广呢? 首先&#xff0c;3D三维展示制作平台能够提供更加…