二叉树是一个重要的数据结构,学习好二叉树很重要,本文将借助leetcode三道练习题,从前序+后序前序+中序以及中序+后序三种遍历组合方式来还原二叉树。

一、二叉树的遍历

首先我们一起来温习下二叉树的三种遍历方式:前序遍历、中序遍历、后续遍历。如果读者不太了解这三种遍历方式,建议找点博客看看二叉树的三种遍历,本文主要是借助二叉树的遍历结果来还原二叉树,所以本文默认读者是了解二叉树的遍历的。

首先我们一起看下二叉树的三种遍历方式,如下图所示一棵二叉树:

三种遍历结果如下所示:

三种遍历方式的区别是:何时遍历。

  • 前序遍历是先遍历,再分别遍历左右子树,左右子树中也是先遍历子树的,简称根左右
  • 中序遍历是先遍历左子树,再遍历,最后遍历右子树,左右子树中也是如此,简称左根右
  • 后续遍历是先遍历左子树,再遍历右子树,最后遍历,左右子树中也是如此,简称左右根

二、根据前序和中序遍历构造二叉树

本小节,我们以前序遍历的结果和中序遍历的结果来还原二叉树,为了文章的完整性,就采用上面的二叉树的遍历结果来进行二叉树的还原,弄懂了这小节,那么就可以将leetcode第105题 从前序与中序遍历序列构造二叉树 做出来,讨论的问题是一样的。

根据上面的前序遍历和中序遍历,该如何正确还原成一棵二叉树呢?其实需要用到前序遍历和中序遍历的两个基本特性:

  • 在前序遍历结果中,第一个位置的元素是二叉树的根节点
  • 在中序遍历结果中,根节点的左边为左子树,根节点的右边为右子树

那么根据这两个特性,我们很容易确定二叉树的左子树和右子树,以及左右子树节点的个数等基本信息。

上图中,在前序遍历中将根节点确定下来之后,在中序遍历中就可以将根节点的左右子树都确定出来。因为前序遍历属于深度优先遍历,也就是“一挖到底”,所以从上图中可以知道左子树的根节点是2,右子树的根节点是3,那么在中序遍历中可以进一步找到左右子树的左右子树,那么这个问题就可以进一步缩小,且符合递归的规律,所以完全可以使用递归来解决这个问题。

为了还原二叉树,我们一起来定义几个变量,方便后续分析树的还原过程:

  • 定义一个Map,用来记录中序遍历结果中个元素与下标索引的对应关系,这样我们可以快速地获取到某个元素在中序遍历结果中的具体位置,比如根节点,我们就可以用根节点将中序遍历结果一分为二,将左右子树都分隔出来,后续左右子树的左右子树也能快速的分隔出来;
  • 定义一个int类型的ri,表示rootIndex,根节点的索引;
  • 定义一个int类型的leftChildTreeNodeNum,表示左子树的节点个数;
  • 在前序遍历结果中定义两个位置变量[ps, pe]ps表示前序遍历结果序列的起始位置,pe表示前序遍历结果序列的结束位置;
  • 在中序遍历结果中定义两个位置变量[is, ie]is表示中序遍历结果序列的起始位置,ie表示中序遍历结果序列的结束位置。

有了上述变量,我们来分析下二叉树的还原过程中的边界情况:
从上图中,我们可以分析成文字如下所示:

  • 在找根节点的时候,都是以这两个整体序列为基础的,所以前序遍历序列的起止位置是[ps, pe],即为[0, preorder.length - 1],中序遍历序列的起止位置是[is, ie],即为[0, inorder.length - 1]
  • 那么在处理左子树的时候,需要在这两个序列中将左子树的部分截取出来,所以在前序遍历序列中的起止位置是[ps + 1, ps + leftChildTreeNodeNum],在中序遍历序列中的起止位置为:[is, ri - 1]
  • 那么在处理右子树的时候,也同样需要在这两个序列中将右子树的部分截取出来,所以在前序遍历序列中的起止位置是[ps + leftChildTreeNodeNum + 1, pe],在中序遍历序列中的起止位置为:[ri + 1, ie]

按照上面的分析结果,边界弄清楚了,那么代码写起来也就方便了,代码如下所示:

/*** 递归法** @param preorder 前序遍历列表* @param inorder 中序遍历列表* @return 二叉树*/
public TreeNode buildTree(int[] preorder, int[] inorder) {Map<Integer, Integer> map = new HashMap<>();for (int i = 0; i < inorder.length; i++) {map.put(inorder[i], i);}return buildTreeHelper(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1, map);
}private TreeNode buildTreeHelper(int[] preorder, int ps, int pe, int[] inorder, int is, int ie,Map<Integer, Integer> map) {// 递归终止条件if (pe < ps || ie < is) {return null;}// 递归本层次需要做的事情// 获取根节点TreeNode root = new TreeNode(preorder[ps]);// 获取根节点在中序遍历结果序列中的位置int ri = map.get(preorder[ps]);// 确定左子树的数量,从而可以从前序遍历中找到左子树和右子树int leftChildTreeNodeNum = ri - is;// 递归过程root.left = buildTreeHelper(preorder, ps + 1, ps + leftChildTreeNodeNum, inorder, is, ri - 1, map);root.right = buildTreeHelper(preorder, ps + leftChildTreeNodeNum + 1, pe, inorder, ri + 1, ie, map);return root;
}

代码看起来是不是很简单?其实这类题只要把规律找到了,边界弄清楚了,写出代码那就是顺理成章的事情了。我们趁热打铁,一起接着看看后面的两小节。

三、根据前序和后序遍历构造二叉树

本小节,我们以前序遍历的结果和后序遍历的结果来还原二叉树,为了文章的完整性,就采用上面的二叉树的遍历结果来进行二叉树的还原,弄懂了这小节,那么就可以将leetcode第889题 根据前序和后序遍历构造二叉树 做出来,讨论的问题是一样的。

根据上面的前序遍历和后序遍历,该如何正确还原成一棵二叉树呢?其实需要用到前序遍历和后序遍历的两个基本特性:

  • 在前序遍历结果中,第一个位置的元素是二叉树的根节点,如果有左子树,那么第二个位置的元素是左子树子树根节点
  • 在后序遍历结果中,最后一个位置的元素是根节点,如果有右子树,那么倒数第二个位置的元素一定是右子树子树根节点

那么根据这两个特性,我们很容易确定二叉树的左子树和右子树,以及左右子树节点的个数等基本信息。

上图中,在前序遍历和后序遍历中将根节点确定下来之后,结合前序遍历和后序遍历的特点,就可以将根节点的左右子树都确定出来。因为前序遍历属于深度优先遍历,也就是“一挖到底”,所以从上图中可以知道左子树的根节点是2,右子树的根节点是3,那么在中前序遍历和后序遍历中可以进一步找到左右子树的左右子树,那么这个问题就可以进一步缩小,且符合递归的规律,所以完全可以使用递归来解决这个问题。

为了还原二叉树,我们一起来定义几个变量,方便后续分析树的还原过程:

  • 定义一个Map,用来记录后序遍历结果中个元素与下标索引的对应关系,这样我们可以快速地获取到某个元素在后序遍历结果中的具体位置,比如左子树的子树根节点,我们就可以用它将后序遍历结果一分为二,将左右子树都分隔出来,后续左右子树的左右子树也能快速的分隔出来;
  • 定义一个int类型的leftRootIndex,表示左子树子树根节点的索引;
  • 定义一个int类型的leftChildTreeNodeNum,表示左子树的节点个数;
  • 在前序遍历结果中定义两个位置变量[ps, pe]ps表示前序遍历结果序列的起始位置,pe表示前序遍历结果序列的结束位置;
  • 在后序遍历结果中定义两个位置变量[pos, poe]pos表示后序遍历结果序列的起始位置,poe表示后序遍历结果序列的结束位置。

有了上述变量,我们来分析下二叉树的还原过程中的边界情况:
从上图中,我们可以分析成文字如下所示:

  • 在找根节点的时候,都是以这两个整体序列为基础的,所以前序遍历序列的起止位置是[ps, pe],即为[0, pre.length - 1],后序遍历序列的起止位置是[pos, poe],即为[0, post.length - 1]
  • 那么在处理左子树的时候,需要在这两个序列中将左子树的部分截取出来,所以在前序遍历序列中的起止位置是[ps + 1, ps + leftChildTreeNodeNum],在后序遍历序列中的起止位置为:[pos, leftRootIndex]
  • 那么在处理右子树的时候,也同样需要在这两个序列中将右子树的部分截取出来,所以在前序遍历序列中的起止位置是[ps + leftChildTreeNodeNum + 1, pe],在后序遍历序列中的起止位置为:[leftRootIndex + 1, poe - 1]

按照上面的分析结果,边界弄清楚了,那么代码写起来也就方便了,代码如下所示:

/*** 递归解法** @param pre 前序遍历序列* @param post 后序遍历序列* @return 还原后的二叉树*/
public TreeNode constructFromPrePost(int[] pre, int[] post) {Map<Integer, Integer> map = new HashMap<>((int) (post.length / 0.75) + 1);for (int i = 0; i < post.length; i++) {map.put(post[i], i);}return buildTreeHelper(pre, 0, pre.length - 1, post, 0, post.length - 1, map);
}private TreeNode buildTreeHelper(int[] pre, int ps, int pe, int[] post, int pos, int poe,Map<Integer, Integer> map) {// 递归终止条件if (pe < ps || poe < pos) {return null;}// 递归本层次需要做的事情// 获取根节点TreeNode root = new TreeNode(pre[ps]);// 获取左子树的根节点在后序遍历序列中的索引// 注意这里有个隐含的边界条件需要判断,判断ps+1是否越界if (ps + 1 > pe) {return root;}int leftRootIndex = map.get(pre[ps + 1]);// 确定左子树的数量,从而可以从前序遍历中找到左子树和右子树int leftChildTreeNodeNum = leftRootIndex - pos + 1;// 递归过程root.left = buildTreeHelper(pre, ps + 1, ps + leftChildTreeNodeNum, post, pos, leftRootIndex, map);root.right =buildTreeHelper(pre, ps + leftChildTreeNodeNum + 1, pe, post, leftRootIndex + 1, poe - 1, map);return root;
}

其实这类题的解题思路都是一样的,把边界弄清楚了,用递归的方式很容易就可以解决问题。

四、根据中序和后序遍历构造二叉树

我们已经一起解决了根据前序和中序,前序和后序的遍历结果序列来还原二叉树,现在我们一起看下这个题型的最后一道题:根据中序和后序的遍历构造二叉树。通过前面两道题的训练,我相信读者都可以独立将这道题做出来,其实思想也是很简单,就是利用中序和后序遍历序列的特性,找到左右子树的边界,那么这道题就基本解决了。读者读到这里,可以暂停下,想想该如何解决这道题。

本小节讨论的问题和leetcode 106题一样:从中序与后序遍历序列构造二叉树 ,我们目前还是以前面的二叉树为例,这里列出中序遍历和后序遍历序列如下:

根据上面的中序序遍历和后序遍历,该如何正确还原成一棵二叉树呢?其实需要用到中序遍历和后序遍历的两个基本特性,我想,说到这, 读者肯定知道它们的特性了,因为前面两小节都已经阐述过了,没错,就是下面两个基本点:

  • 在后序遍历结果中,最后一个位置的元素是二叉树的根节点
  • 在中序遍历结果中,根节点的左边为左子树,根节点的右边为右子树

那么根据这两个特性,我们很容易确定二叉树的左子树和右子树,以及左右子树节点的个数等基本信息。
我们从后序遍历中可以直接定位到整棵树的根节点,就是后序遍历序列中的最后一个位置的元素,在中序遍历中找到根节点的位置,以根节点为划分线,可以将中序遍历序列一分为二,左边是左子树,右边是右子树。

我们定义几个变量,如下所示:

  • 定义一个Map,用来记录中序遍历结果中个元素与下标索引的对应关系,这样我们可以快速地获取到某个元素在中序遍历结果中的具体位置,比如根节点,我们就可以用根节点将中序遍历结果一分为二,将左右子树都分隔出来,后续左右子树的左右子树也能快速的分隔出来;

  • 定义一个int类型的ri,表示rootIndex,根节点在中序遍历中的索引;

  • 定义一个int类型的leftChildTreeNodeNum,表示左子树的节点个数;

  • 在中序遍历结果中定义两个位置变量[is, ie]is表示中序遍历结果序列的起始位置,ie表示中序遍历结果序列的结束位置;

  • 在后序遍历结果中定义两个位置变量[pos, poe]pos表示前序遍历结果序列的起始位置,poe表示前序遍历结果序列的结束位置。

    从上图中,我们可以分析成文字如下所示:

  • 在找根节点的时候,都是以这两个整体序列为基础的,所以中序遍历序列的起止位置是[is, ie],即为[0, inorder.length - 1],后序遍历序列的起止位置是[pos, poe],即为[0, postorder.length - 1]

  • 那么在处理左子树的时候,需要在这两个序列中将左子树的部分截取出来,所以在中序遍历序列中的起止位置是[is, ri - 1],在后序遍历序列中的起止位置为:[pos, pos + leftChildTreeNodeNum - 1]

  • 那么在处理右子树的时候,也同样需要在这两个序列中将右子树的部分截取出来,所以在中序遍历序列中的起止位置是[ri + 1, ie],在后序遍历序列中的起止位置为:[pos + leftChildTreeNodeNum, poe - 1]

按照上面的分析结果,边界弄清楚了,那么代码写起来也就方便了,代码如下所示:

/*** 递归解法** @param inorder 中序遍历序列* @param postorder 后序遍历序列* @return 还原后的二叉树*/
public TreeNode buildTree(int[] inorder, int[] postorder) {Map<Integer, Integer> indexContainer = new HashMap<>((int) (inorder.length / 0.75) + 1);for (int i = 0; i < inorder.length; i++) {indexContainer.put(inorder[i], i);}return buildTreeHelper(inorder, 0, inorder.length - 1, postorder, 0, postorder.length - 1, indexContainer);
}private TreeNode buildTreeHelper(int[] inorder, int is, int ie, int[] postorder, int pos, int poe,Map<Integer, Integer> map) {// 如果postorder为空,直接返回nullif (ie < is || poe < pos) {return null;}// 获取根节点TreeNode root = new TreeNode(postorder[poe]);int ri = map.get(postorder[poe]);// 获取左子树的节点个数,这样就可以在后序遍历列表中确定左右子树int leftTreeNodeNum = ri - is;// 确定左右子树root.left = buildTreeHelper(inorder, is, ri - 1, postorder, pos, pos + leftTreeNodeNum - 1, map);root.right = buildTreeHelper(inorder, ri + 1, ie, postorder, pos + leftTreeNodeNum, poe - 1, map);return root;
}

五、总结一下

二叉树的还原至少需要知道三种遍历方式中的两种才可以正确还原,如果只知道其中一个,那么被还原出来的二叉树可能存在多个。其实还原二叉树这类题型倒是没什么难度,主要是需要弄清边界,理清二叉树的特性,那么问题将迎刃而解!

读完本文,你可以将文中代码直接粘贴到leetcode中就可以直接运行,涉及的三道题在这里再列一下:

  • No.104 从前序与中序遍历序列构造二叉树
  • No.889 根据前序和后序遍历构造二叉树
  • No.106 从中序与后序遍历序列构造二叉树

欢迎读者评论交流~

【甘泉算法】一文搞定还原二叉树问题相关推荐

  1. 一文搞定面试中的二叉树问题

    一文搞定面试中的二叉树问题 版权所有,转载请注明出处,谢谢! http://blog.csdn.net/walkinginthewind/article/details/7518888 树是一种比较重 ...

  2. 【语义分割】综述——一文搞定语义分割

    本文记录了博主阅读的关于语义分割(Semantic Segmentation)的综述类文章的笔记,更新于2019.02.19. [语义分割]综述--一文搞定语义分割 参考文献网址 An overvie ...

  3. php带参数单元测试_一文搞定单元测试核心概念

    基础概念 单元测试(unittesting),是指对软件中的最小可测试单元进行检查和验证,这里的最小可测试单元通常是指函数或者类.单元测试是即所谓的白盒测试,一般由开发人员负责测试,因为开发人员知道被 ...

  4. 【Python基础】一文搞定pandas的数据合并

    作者:来源于读者投稿 出品:Python数据之道 一文搞定pandas的数据合并 在实际处理数据业务需求中,我们经常会遇到这样的需求:将多个表连接起来再进行数据的处理和分析,类似SQL中的连接查询功能 ...

  5. 一文搞定Swing和Qt按钮和文本框的创建

    一文搞定Swing和Qt按钮和文本框的创建 Qt的截图 java的 源码 package com.lujun;import java.awt.Container;import javax.swing. ...

  6. 一文搞定C#关于NPOI类库的使用读写Excel以及io流文件的写出

    一文搞定C#关于NPOI类库的使用读写Excel以及io流文件的写出 今天我们使用NPOI类库读写xlsx文件, 最终实现的效果如图所示 从太平洋官网下载相应的类库,大概4~5MB,不要从github ...

  7. 一文搞定Qt读写excel以及qt读写xml数据

    一文搞定Qt读写excel以及qt读写xml数据 最终的实现效果图 RC_ICONS = logo.ico .pro文件同级目录下加入 logo.ico 图标文件,运行文件,文件的图标就被写入软件 u ...

  8. 一文搞定 Spring Data Redis 详解及实战

    转载自  一文搞定 Spring Data Redis 详解及实战 SDR - Spring Data Redis的简称. Spring Data Redis提供了从Spring应用程序轻松配置和访问 ...

  9. 【全网最全】一文搞定 Linux 压缩、解压哪些事儿

    一文搞定 Linux 压缩.解压哪些事儿 Linux 常用的解压和压缩命令如下: 1..tar # 解包 tar xvf FileName.tar # 打包 tar cvf FileName.tar ...

最新文章

  1. 【工业智能】人工智能真的无所不能吗?
  2. PHP上传图片到数据库和存储到本地文件夹的方法
  3. 《Deep Learning With Python second edition》英文版读书笔记:第十一章DL for text: NLP、Transformer、Seq2Seq
  4. CRMEB系统使用协议
  5. Core Services层
  6. JavaDoc命令使用说明
  7. OpenSitUp开源项目:零基础开发基于姿态估计的运动健身APP
  8. Jupyter 快速入门
  9. 放弃使用 15 年的 macOS,我决定换成 Linux!
  10. 计算机图形学全代码,计算机图形学作业参考代码
  11. java常用类objet,Java基础-常用API-Object类
  12. C++ Reference: Standard C++ Library reference: C Library: cfenv: FE_INEXACT
  13. ubuntu php代码编辑器,Linux_ubuntu16.04编辑器vi该怎么使用?,vi编辑器,ubuntu中最基本的文 - phpStudy...
  14. 【面经】2018金山WPS前端笔试题 面试题
  15. 【信息技术】【2003.03】视觉监控应用中人体跟踪算法的设计与实现
  16. JLINK仿真器与ST-LINK仿真器的安装与配置.pdf
  17. 自然数拆分(回溯法)
  18. Word文档中,文字下面的波浪线怎么去掉
  19. ARM与嵌入式Linux的入门建议
  20. 软件公司如何提升效能?研发团队的北极星指标

热门文章

  1. overlay filesystem
  2. Prometheus和Zabbix的对比
  3. 我感觉你要弄无人机+激光雷达,可以先在车子上实现,再放到无人机上应该很快。
  4. 好用的云函数!后端低代码接口开发,零基础编写API接口
  5. python字符串不为空的判断_python判断是否为空字符串的方法
  6. python一元线性回归的优点_Python实现机器学习一(实现一元线性回归)
  7. 在线音乐播放系统测试文档
  8. liunx下安装mysql
  9. mfc-7360扫描时无法检查连接计算机,mfc7360怎么扫描 mfc7360扫描键无反映解决办法...
  10. 海上钢琴师--豆瓣评论