105. Construct Binary Tree from Preorder and Inorder Traversal (Medium)
LeetCode 题解 | 从前序与中序遍历序列构造二叉树
前言:毕业后头一回这么认真写题解,话
可能有点多,如果有难以理解、有误的地方欢迎各位大佬指出
题目描述
根据一棵树的前序遍历与中序遍历构造二叉树(假定无重复值)
复盘
这题主要考察了两点:
- 数据结构:
二叉树
、前序遍历
、中序遍历
的定义 - 算法:
递归(分治思想)
数据结构
|
|
前序遍历:根、左、右 —— [3,9,20,15,7]
中序遍历:左、根、右 —— [9,3,15,20,7]
算法
递归
定义:把原问题拆解成若干个与原问题结构相同但规模更小的子问题,待子问题解决以后,原问题就得以解决。
说人话:好比拼 100*100 的拼图,拆成拼 100 份 10*10 的拼图,等 100 份子拼图拼完后,原拼图也拼完了(假设每个子拼图素材完备)。
在复盘这道题时,我是这么拆解这题的递归过程的:递归目标
、递归逻辑
、递归参数
、初始状态
、结束状态
注:下面的版本 a 和版本 b 都是我看了答案后做的复盘,而在看别人的题解和代码的时候,可以同时用测试数据代入进去(重点可以运行一下第一、第二步以及最后两步),会有更直观的感受。
版本 a
在参考了力扣的官方视频题解后,完成了版本 a
递归目标
整道题的最终目标是构建一棵树
,用递归的思想,要把一棵树拆成结构相同
但规模更小
,那就是构建树的一个节点
。
所以,子问题就是:返回下一个节点 ITreeNode
(根 ,左子树,右子树),直到叶子节点结束
递归逻辑
我们知道了递归的目标是找到当前节点的下一个节点
,而由节点的定义可知,构建一个节点需要确定三个成员:根
、左子树
、右子树
。
所以每一次递归的逻辑实际上就是寻找:
- 找前序的根。找到
根节点
—— 前序遍历的第一个元素 - 找中序的根。根据
前序遍历根节点的值
,找到中序遍历中对应的元素
—— 题目告知节点的值不会重复,所以能根据前序遍历的根节点一一对应到中序遍历的根节点 - 找前序、中序的左、右子树。根据中序遍历的根节点(左子树、根、右子树),确定前序遍历、中序遍历各自的左子树、右子树(各自
起始位置
、结束位置
) - 重复以上步骤直到叶子节点结束
递归参数
根据递归逻辑,可以得知,只要确定了前序遍历、中序遍历的根节点
,前序、中序的左、右子树(各自起始位置
、结束位置
),就可以确定下一个节点
。
所以递归参数:preorder、preLeft、preRight、inorder、inLeft、inRight、map
引入 map 来记录 inorder 值与下标的映射,从而优化根据前序遍历根节点的值
,找到中序遍历中对应的元素
的速度
初始状态
方法
在纸上画出第一次的初始状态
|
|
结束条件
方法
- 先在纸上画出最后一次递归的情况
- 再在纸上画出倒数第二次的情况
通过第1、2步,可以得出递归终止的条件
说明
- 最后一次递归即
叶子节点
,子树为空,即preorder
在[preLeft, preRight]
区间内长度为 0,而如果preLeft
===preRight
表示当前节点并不是叶子节点,仍有 1 个子节点(叶子节点)。 - 由第一步可知,倒数第二次时,
preLeft
===preRight
代码
|
|
复杂度分析
时间复杂度
O(n) 其中 n 是树中的节点个数。
空间复杂度
O(n) 返回的答案 O(n) + 哈希 map O(n) + 每次遍历时 var x = inRootIndex + preLeft - inLeft;
O(n)?,
其中 n 是树中的节点个数
版本 b
早上我在查看代码运行速度排名图表时,无意间看到鼠标样式变成手型了,于是点了一下
结果弹出来了一份代码……
看到这份代码比我快,而且又很简洁,于是就想研究研究.
递归逻辑
友情提示:如果感觉下面的文字有点绕,可以先略过看看下一部分的内容
取前序根节点
,并移除该根节点
在说递归逻辑之前,先想象一下,不断地移除前序遍历的第一个元素,会发生什么?
根据[根、左、右]
的定义,可知会先移除根节点,然后是左子树,最后是右子树。
举个例子,上图前序遍历:根、左、右 —— [3,9,20,15,7]
第一次移除根
3、第二次左
9、第三次就是右
20 了,然后再移除左
15,最后移除右
7
所以每一次递归,前序遍历移除第一个元素后,可以保证余下序列第一个元素始终是前序遍历根节点,且只有2种可能:
- 左子树的根节点
- 右子树的根节点
从而就不需要额外的参数去记录前序遍历的根节点的下标了,直接 preorder.shift()
即可。
找中序对应的根节点
而对于这道题,题目条件给出了中序遍历
,根据定义,前序遍历的根节点是与中序遍历的根节点一一对应,从而可以得出中序遍历根
节点的左
、右
子树明确的范围。
构建左子树
、右子树
又因为定义:前序 [根、左、右]
, 中序 [左、根、右]
前序的左、右子树的长度和中序左、右子树的长度也是一致的,所以当中序遍历的左、右子树构建完成后,对应的前序遍历左、右子树也一样完成了。
所以这个时候,与版本 a 不同,我们转换思路,不去考虑前序遍历的左、右子树边界,只考虑中序遍历的左右子树边界,而当中序遍历的左、右子树为 null 时,这个节点就构建完成了。
小结
所以步骤就是:
- 取
前序根节点
,并移除该根节点
- 找
中序对应的根节点
- 构建
左子树
- 重复以上步骤直到没有
左子树
- 构建
右子树
- 重复以上步骤直到没有
右子树
- 结束
ps:这道题用这个递归逻辑也可以很方便地解决 106.construct-binary-tree-from-inorder-and-postorder-traversal 这题。
递归参数
preorder(会变)、inorder、start、end、map
- 引入 map 来优化
start
记录中序遍历
的左子树开始位置end
记录中序遍历
的右子树结束位置
初始状态
方法
在纸上画出第一次的初始状态
|
|
结束条件
方法
- 先在纸上画出最后一次递归的情况
- 再在纸上画出倒数第二次的情况
通过第1、2步,可以得出递归终止的条件
说明
- 最后一次递归即
叶子节点
,子树为空,即inorder
在[start, end]
区间内长度为 0,而如果start
===end
表示当前节点并不是叶子节点,仍有 1 个子节点(叶子节点)。 - 由第一步可知,倒数第二次时,
start
===end
AC 代码
2020年07月10日09:00:00
|
|
复杂度分析
时间复杂度
O(n) 其中 n 是树中的节点个数。
ps:
preorder.shift();
这个的复杂度未知…… 但版本 b 的速度比版本 a 的快60ms
,所以应该也是 O(n) 吧…… 可能内部有优化?
空间复杂度
O(n) 返回的答案 O(n) + 哈希 map O(n) + 每次遍历时 let index = map[val];
O(n)?,
其中 n 是树中的节点个数
统计
版本 | 耗时 | 超越 | 内存 | 超越 | 更新时间 |
---|---|---|---|---|---|
a 1.0.0 | 136ms | 33.86% | 39.3MB | 39.18% | 2020年07月08日20:00:00 |
b 1.0.0 | 72ms | 96.35% | 40.5MB | 34.13% | 2020年07月10日09:00:00 |
数据来自 leetcode.com 提交记录
参考资料
总结
写完这篇题解后,感觉解这道题的关键主要还是在于看答案理解 + 利用定义
- 前序遍历(根、左、右)
- 中序遍历(左、根、右)
至于递归逻辑,在纸上画画图,手动算两轮测试数据,有很大的帮助。
如果有难以理解、有错误的地方欢迎各位大佬指出
感谢读到这里~
以上