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

news2024/11/29 2:38:39

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

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

举例说明:

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

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());
    }
};

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


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

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

老师怎样克服公开课的心理恐惧?

公开课是老师进修学习和交流教学经验的必要手段&#xff0c;但是&#xff0c;很多老师在面对公开课时会出现心理恐惧&#xff0c;在讲台上发挥不自如&#xff0c;影响教学效果。下面就是一些克服公开课心理恐惧的方法&#xff1a; 一、充分准备 准备充分是心理恐惧的最好解决方…

2023.12.4 关于 Spring Boot 统一异常处理

目录 引言 统一异常处理 异常全部监测 引言 将异常处理逻辑集中到一个地方&#xff0c;可以避免在每个控制器或业务逻辑中都编写相似的异常处理代码&#xff0c;这降低了代码的冗余&#xff0c;提高了代码的可维护性统一的异常处理使得调试和维护变得更加容易&#xff0c;通…

deepflow本地部署过程

本地服务器配置&#xff0c;32C&#xff0c;48G内存 整个过程需要配置k8s&#xff0c;安装helm, 安装grafana, 安装deepflow以及deepflow-ctl&#xff0c;以及部署demo 在采用sealos进行ALL-IN-ONE部署之前&#xff0c; grafana 先安装它 wget -q -O /usr/share/keyrings/gr…

VSCode + gdb + gdbserver调试ARM程序

在开发ARM嵌入式端C/C程序时&#xff0c;一般会在PC上编写代码&#xff0c;在Linux服务器上编译&#xff0c;然后将程序复制或挂载到ARM开发板上运行。如果程序出了问题&#xff0c;在不使用gdb的情况下&#xff0c;经常在代码中添加打印&#xff0c;编译&#xff0c;然后在开发…

nodejs+vue+ElementUi小区社区公寓宿舍智能访客预约系统

该系统将采用B/S结构模式&#xff0c;前端部分主要使用html、css、JavaScript等技术&#xff0c;使用Vue和ElementUI框架搭建前端页面&#xff0c;后端部分将使用Nodejs来搭建服务器&#xff0c;并使用MySQL建立后台数据系统&#xff0c;通过axios完成前后端的交互&#xff0c;…

内网渗透Dump Hash之NTDS.dit

Ntds.dit 在活动⽬录中&#xff0c;所有的数据都保存在域控的ntds.dit⽂件中。 ntds.dit是⼀ 个⼆进制⽂件&#xff0c;⽂件路径为域控的%SystemRoot%\ntds\ntds.dit。NTDS.dit 包 含不限于⽤户名、散列值、组、GPP、OU等活动⽬录的信息。系统运维⼈员可以使⽤VSS实 现对该⽂件…

金蝶云星空单据编辑界面,不允许批量填充操作【分条件】

文章目录 金蝶云星空单据编辑界面&#xff0c;不允许批量填充操作【分条件】前提说明案例演示开发设计测试填充值清空值 金蝶云星空单据编辑界面&#xff0c;不允许批量填充操作【分条件】 前提说明 上一个文章的设计&#xff0c;不管是填充值&#xff0c;还是清空值都一律不…

【matlab程序】matlab画螺旋图|旋转图

%% 数学之美====》螺旋线 % 海洋与大气科学 % 20231205 clear;clc;close all; n=10; t=0:0.01:2pin; R=1; xx=nan(length(t),1);yy=nan(length(t),1); for i=1:length(t) xx(i)=Rcos(t(i)); yy(i)=Rsin(t(i)); R=R+1; end figure set(gcf,‘position’,[50 50 1200 1200],‘col…

基于PIPNet的人脸106关键点检测

做美颜需要使用到人脸关键点&#xff0c;所以整理了一下最近的想法。 按模型结构分类&#xff1a; 1.Top-Down: 分为两个步骤&#xff0c;首先&#xff0c;对于原始输入图片做目标检测&#xff0c;比如做人脸检测&#xff0c;将人脸区域抠出&#xff0c;单独送进关键点检测模…

sql-SQL练习生

推荐一款inscode内的模板SQL练习生&#xff0c;此文附带目前所有题的答案 如有错误欢迎斧正~ https://inscode.csdn.net/TPEngineer/SQLBoy 为了更好的体验&#xff0c;请按下面的方法打开&#xff1a; 1.运行一下 2.等待加载 3.在网页打开 温馨提醒&#xff1a;此处做题不会保…

【交叉编译】

一、什么是交叉编译 二、为什么要交叉编译&#xff1f; 三、交叉编译要用到的工具&#xff08;工具链、交叉编译器&#xff09; 四、交叉编译工具链的安装 五、配置环境变量 六、交叉编译工具编译 七、带wiringPi库的交叉编译如何进行 八、软链接、硬链接 九、Linux创建链接命令…

nodejs+vue+ElementUi牙科诊所信息化系统

该系统将采用B/S结构模式&#xff0c;前端部分主要使用html、css、JavaScript等技术&#xff0c;使用Vue和ElementUI框架搭建前端页面&#xff0c;后端部分将使用Nodejs来搭建服务器&#xff0c;并使用MySQL建立后台数据系统&#xff0c;通过axios完成前后端的交互&#xff0c;…

认识异常 ---java

目录 一. 异常的概念 二. 异常的体系结构 三. 异常的分类 三. 异常的处理 3.1 异常的抛出throw 3.2. 异常声明throws 3.3 捕获并处理try-catch finally 3.4异常的处理流程 四. 自定义异常类 一. 异常的概念 在 Java 中&#xff0c;将程序执行过程中发生的不正常行为称为…

CPP-SCNUOJ-Problem P24. [算法课贪心] 跳跃游戏

Problem P24. [算法课贪心] 跳跃游戏 给定一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。 数组中的每个元素代表你在该位置可以跳跃的最大长度 判断你是否能够到达最后一个下标。 输入 输入一行数组nums 输出 输出true/fasle 样例 标准输入 2 3 1 …

出海风潮:中国母婴品牌征服国际市场的机遇与挑战!

近年来&#xff0c;中国母婴品牌在国内市场蓬勃发展的同时&#xff0c;也逐渐将目光投向国际市场。这一趋势不仅受益于中国经济的崛起&#xff0c;还得益于全球市场对高质量母婴产品的不断需求。然而&#xff0c;面对国际市场的机遇&#xff0c;中国母婴品牌同样面临着一系列挑…

Myblog02-基于ssm,springboot的改进

目录 一、项目概述&#xff1a; 应用技术&#xff1a; 接口实现&#xff1a; 数据库建表&#xff0c;sql脚本&#xff1a; 页面展示&#xff1a;登陆页面 项目源码&#xff1a;myblog01: 初版的个人博客项目-使用基本的javaWeb (gitee.com) 二、对博客系统进行测试 总结…

深入分析爬虫中time.sleep和Request的并发影响

背景介绍 在编写Python爬虫程序时&#xff0c;我们经常会遇到需要控制爬取速度以及处理并发请求的情况。本文将深入探讨Python爬虫中使用time.sleep()和请求对象时可能出现的并发影响&#xff0c;并提供解决方案。 time.sleep()介绍 首先&#xff0c;让我们来了解一下time.s…

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

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

CETN03 - The Evolution of Computers

文章目录 I. IntroductionII. First Modern Digital Computer: ENIAC (1946)III. First Generation ComputerIV. Second Generation ComputerV. Third Generation ComputerVI. Fourth Generation ComputerVII. ConclusionI. 引言II. 第一台现代数字计算机&#xff1a;ENIAC&…

自定义 el-select 和 el-input 样式

文章目录 需求分析el-select 样式el-input 样式el-table 样式 需求 自定义 选择框的下拉框的样式和输入框 分析 el-select 样式 .select_box{// 默认placeholder:deep .el-input__inner::placeholder {font-size: 14px;font-weight: 500;color: #3E534F;}// 默认框状态样式更…