本文参考labuladong算法笔记[东哥带你刷二叉树(构造篇) | labuladong 的算法笔记]
二叉树解题的思维模式分两类:
1、是否可以通过遍历一遍二叉树得到答案?如果可以,用一个 traverse
函数配合外部变量来实现,这叫「遍历」的思维模式。
2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。
无论使用哪种思维模式,你都需要思考:
如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前/中/后序位置)做?其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。
1、概述
二叉树的构造问题一般都是使用「分解问题」的思路:构造整棵树 = 根节点 + 构造左子树 + 构造右子树。
2、实战
654. 构造最大二叉树
给定一个不重复的整数数组 nums
。 最大二叉树 可以用下面的算法从 nums
递归地构建:
- 创建一个根节点,其值为
nums
中的最大值。 - 递归地在最大值 左边 的 子数组前缀上 构建左子树。
- 递归地在最大值 右边 的 子数组后缀上 构建右子树。
返回 nums
构建的 最大二叉树 。
示例 1:
输入:nums = [3,2,1,6,0,5] 输出:[6,3,5,null,2,0,null,null,1] 解释:递归调用如下所示: - [3,2,1,6,0,5] 中的最大值是 6 ,左边部分是 [3,2,1] ,右边部分是 [0,5] 。 - [3,2,1] 中的最大值是 3 ,左边部分是 [] ,右边部分是 [2,1] 。 - 空数组,无子节点。 - [2,1] 中的最大值是 2 ,左边部分是 [] ,右边部分是 [1] 。 - 空数组,无子节点。 - 只有一个元素,所以子节点是一个值为 1 的节点。 - [0,5] 中的最大值是 5 ,左边部分是 [0] ,右边部分是 [] 。 - 只有一个元素,所以子节点是一个值为 0 的节点。 - 空数组,无子节点。
示例 2:
输入:nums = [3,2,1] 输出:[3,null,2,null,1]
提示:
1 <= nums.length <= 1000
0 <= nums[i] <= 1000
nums
中的所有整数 互不相同
【思路】
# 函数签名如下
def constructMaximumBinaryTree(nums: List[int]) -> TreeNode:
每个二叉树节点都可以认为是一棵子树的根节点,对于根节点,首先要做的当然是把想办法把自己先构造出来,然后想办法构造自己的左右子树。
所以,我们要遍历数组把找到最大值 maxVal
,从而把根节点 root
做出来,然后对 maxVal
左边的数组和右边的数组进行递归构建,作为 root
的左右子树。
按照题目给出的例子,输入的数组为 [3,2,1,6,0,5]
,对于整棵树的根节点来说,其实在做这件事:
def constructMaximumBinaryTree([3,2,1,6,0,5]) -> TreeNode:
# 找到数组中的最大值
root = TreeNode(6)
# 递归调用构造左右子树
root.left = constructMaximumBinaryTree([3,2,1])
root.right = constructMaximumBinaryTree([0,5])
return root
# 当前 nums 中的最大值就是根节点,然后根据索引递归调用左右数组构造左右子树即可
# 再详细一点,就是如下伪码
def constructMaximumBinaryTree(nums: List[int]) -> TreeNode:
if not nums:
return None
# 找到数组中的最大值
maxVal = max(nums)
index = nums.index(maxVal)
root = TreeNode(maxVal)
# 递归调用构造左右子树
root.left = constructMaximumBinaryTree(nums[:index])
root.right = constructMaximumBinaryTree(nums[index+1:])
return root
当前 nums
中的最大值就是根节点,然后根据索引递归调用左右数组构造左右子树即可。
明确了思路,我们可以重新写一个辅助函数 build
,来控制 nums
的索引:
【python 解法】
class Solution:
def constructMaximumBinaryTree(self, nums: List[int]) -> TreeNode:
return self.build(nums, 0, len(nums) - 1)
# 定义:将 nums[lo..hi] 构造成符合条件的树,返回根节点
def build(self, nums: List[int], lo: int, hi: int) -> TreeNode:
# base case
if lo > hi:
return None
# 找到数组中的最大值和对应的索引
index = -1
maxVal = float('-inf')
for i in range(lo, hi + 1):
if maxVal < nums[i]:
index = i
maxVal = nums[i]
# 先构造出根节点
root = TreeNode(maxVal)
# 递归调用构造左右子树
root.left = self.build(nums, lo, index - 1)
root.right = self.build(nums, index + 1, hi)
return root
105. 从前序和中序遍历序列构造二叉树
给定两个整数数组 preorder
和 inorder
,其中 preorder
是二叉树的先序遍历, inorder
是同一棵树的中序遍历,请构造二叉树并返回其根节点。
示例 1:
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7] 输出: [3,9,20,null,null,15,7]
示例 2:
输入: preorder = [-1], inorder = [-1] 输出: [-1]
提示:
1 <= preorder.length <= 3000
inorder.length == preorder.length
-3000 <= preorder[i], inorder[i] <= 3000
preorder
和inorder
均 无重复 元素inorder
均出现在preorder
preorder
保证 为二叉树的前序遍历序列inorder
保证 为二叉树的中序遍历序列
【思路】
# 函数签名如下
def buildTree(preorder: List[int], inorder: List[int]):
废话不多说,直接来想思路,首先思考,根节点应该做什么。
类似上一题,我们肯定要想办法确定根节点的值,把根节点做出来,然后递归构造左右子树即可。
我们先来回顾一下,前序遍历和中序遍历的结果有什么特点?
def traverse(root):
if not root:
return
# 前序遍历
preorder.append(root.val)
traverse(root.left)
traverse(root.right)
def traverse(root):
if not root:
return
traverse(root.left)
# 中序遍历
inorder.append(root.val)
traverse(root.right)
后文 二叉树就那几个框架 写过,这样的遍历顺序差异,导致了 preorder
和 inorder
数组中的元素分布有如下特点:
找到根节点是很简单的,前序遍历的第一个值 preorder[0]
就是根节点的值。
关键在于如何通过根节点的值,将 preorder
和 inorder
数组划分成两半,构造根节点的左右子树?
换句话说,对于以下代码中的 ?
部分应该填入什么:
def buildTree(preorder, inorder):
# 根据函数定义,用 preorder 和 inorder 构造二叉树
return build(preorder, 0, len(preorder) - 1,
inorder, 0, len(inorder) - 1)
# build 函数的定义:
# 若前序遍历数组为 preorder[preStart..preEnd],
# 中序遍历数组为 inorder[inStart..inEnd],
# 构造二叉树,返回该二叉树的根节点
def build(preorder, preStart, preEnd,
inorder, inStart, inEnd):
# root 节点对应的值就是前序遍历数组的第一个元素
rootVal = preorder[preStart]
# rootVal 在中序遍历数组中的索引
index = 0
for i in range(inStart, inEnd + 1):
if inorder[i] == rootVal:
index = i
break
root = TreeNode(rootVal)
# 递归构造左右子树
root.left = build(preorder, ?, ?,
inorder, ?, ?)
root.right = build(preorder, ?, ?,
inorder, ?, ?)
return root
对于代码中的 rootVal
和 index
变量,就是下图这种情况:
另外,也有读者注意到,通过 for 循环遍历的方式去确定 index
效率不算高,可以进一步优化。
因为题目说二叉树节点的值不存在重复,所以可以使用一个 HashMap 存储元素到索引的映射,这样就可以直接通过 HashMap 查到 rootVal
对应的 index
:
# 存储 inorder 中值到索引的映射
val_to_index = {}
def build_tree(preorder, inorder):
for i in range(len(inorder)):
val_to_index[inorder[i]] = i
return build(preorder, 0, len(preorder) - 1,
inorder, 0, len(inorder) - 1)
def build(preorder, pre_start, pre_end,
inorder, in_start, in_end):
root_val = preorder[pre_start]
# 避免 for 循环寻找 rootVal
index = val_to_index[root_val]
# ...
现在我们来看图做填空题,下面这几个问号处应该填什么:
root.left = build(preorder, ?, ?,
inorder, ?, ?)
root.right = build(preorder, ?, ?,
inorder, ?, ?)
对于左右子树对应的 inorder
数组的起始索引和终止索引比较容易确定:
对于 preorder
数组呢?如何确定左右数组对应的起始索引和终止索引?
这个可以通过左子树的节点数推导出来,假设左子树的节点数为 leftSize
,那么 preorder
数组上的索引情况是这样的:
看着这个图就可以把 preorder
对应的索引写进去了:
leftSize = index - inStart
root.left = build(preorder, preStart + 1, preStart + leftSize,
inorder, inStart, index - 1)
root.right = build(preorder, preStart + leftSize + 1, preEnd,
inorder, index + 1, inEnd)
【python 解法】
class Solution:
# 存储 inorder 中值到索引的映射
valToIndex = dict()
def buildTree(self, preorder, inorder):
for i in range(len(inorder)):
self.valToIndex[inorder[i]] = i
return self.build(preorder, 0, len(preorder) - 1,
inorder, 0, len(inorder) - 1)
# build 函数的定义:
# 若前序遍历数组为 preorder[preStart..preEnd],
# 中序遍历数组为 inorder[inStart..inEnd],
# 构造二叉树,返回该二叉树的根节点
def build(self, preorder, preStart, preEnd,
inorder, inStart, inEnd):
if preStart > preEnd:
return None
# root 节点对应的值就是前序遍历数组的第一个元素
rootVal = preorder[preStart]
# rootVal 在中序遍历数组中的索引
index = self.valToIndex[rootVal]
leftSize = index - inStart
# 先构造出当前根节点
root = TreeNode(rootVal)
# 递归构造左右子树
root.left = self.build(preorder, preStart + 1, preStart + leftSize,
inorder, inStart, index - 1)
root.right = self.build(preorder, preStart + leftSize + 1, preEnd,
inorder, index + 1, inEnd)
return root
我们的主函数只要调用 build
函数即可,你看着函数这么多参数,解法这么多代码,似乎比我们上面讲的那道题难很多,让人望而生畏,实际上呢,这些参数无非就是控制数组起止位置的,画个图就能解决了。
106. 从后序和中序遍历序列构造二叉树
给定两个整数数组 inorder
和 postorder
,其中 inorder
是二叉树的中序遍历, postorder
是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。
示例 1:
输入:inorder = [9,3,15,20,7], postorder = [9,15,7,20,3] 输出:[3,9,20,null,null,15,7]
示例 2:
输入:inorder = [-1], postorder = [-1] 输出:[-1]
提示:
1 <= inorder.length <= 3000
postorder.length == inorder.length
-3000 <= inorder[i], postorder[i] <= 3000
inorder
和postorder
都由 不同 的值组成postorder
中每一个值都在inorder
中inorder
保证是树的中序遍历postorder
保证是树的后序遍历
【思路】
# 函数签名如下
def buildTree(inorder: List[int], postorder: List[int]) -> TreeNode:
类似的,看下后序和中序遍历的特点:
def traverse(root):
if root:
traverse(root.left)
traverse(root.right)
# 后序遍历
postorder.append(root.val)
def traverse(root):
if root:
traverse(root.left)
# 中序遍历
inorder.append(root.val)
traverse(root.right)
这样的遍历顺序差异,导致了 postorder
和 inorder
数组中的元素分布有如下特点:
这道题和上一题的关键区别是,后序遍历和前序遍历相反,根节点对应的值为 postorder
的最后一个元素。
整体的算法框架和上一题非常类似,我们依然写一个辅助函数 build
:
class Solution:
def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode:
# 存储 inorder 中值到索引的映射
valToIndex = {val: idx for idx, val in enumerate(inorder)}
# build 函数的定义:
# 后序遍历数组为 postorder[postStart..postEnd],
# 中序遍历数组为 inorder[inStart..inEnd],
# 构造二叉树,返回该二叉树的根节点
def build(in_left, in_right, post_left, post_right):
if in_left > in_right: return
# root 节点对应的值就是后序遍历数组的最后一个元素
root_val = postorder[post_right]
# rootVal 在中序遍历数组中的索引
index = valToIndex[root_val]
root = TreeNode(root_val)
# 递归构造左右子树
size_left_subtree = index - in_left
root.left = build(inorder, ?, ?,
postorder, ?, ?)
root.right = build(inorder, ?, ?,
postorder, ?, ?)
return root
return build(0, len(inorder) - 1, 0, len(postorder) - 1)
现在 postoder
和 inorder
对应的状态如下:
我们可以按照上图将问号处的索引正确填入:
leftSize = index - inStart
root.left = build(inorder, inStart, index - 1,
postorder, postStart, postStart + leftSize - 1)
root.right = build(inorder, index + 1, inEnd,
postorder, postStart + leftSize, postEnd - 1)
【Python 解法】
class Solution:
# 存储 inorder 中值到索引的映射
valToIndex = {}
def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode:
for i in range(len(inorder)):
self.valToIndex[inorder[i]] = i
return self.build(inorder, 0, len(inorder) - 1,
postorder, 0, len(postorder) - 1)
# build 函数的定义:
# 后序遍历数组为 postorder[postStart..postEnd],
# 中序遍历数组为 inorder[inStart..inEnd],
# 构造二叉树,返回该二叉树的根节点
def build(self, inorder, inStart, inEnd, postorder, postStart, postEnd):
if inStart > inEnd:
return None
# root 节点对应的值就是后序遍历数组的最后一个元素
rootVal = postorder[postEnd]
# rootVal 在中序遍历数组中的索引
index = self.valToIndex[rootVal]
# 左子树的节点个数
leftSize = index - inStart
root = TreeNode(rootVal)
# 递归构造左右子树
root.left = self.build(inorder, inStart, index - 1, postorder, postStart, postStart + leftSize - 1)
root.right = self.build(inorder, index + 1, inEnd, postorder, postStart + leftSize, postEnd - 1)
return root
# 也可以通过计算right_size来分割找起始点
# right_size = inend - index
#
# root = TreeNode(root_val)
# root.left = self.build(inorder, instart, index-1,\
# postorder, poststart, postend - right_size - 1)
# root.right = self.build(inorder, index + 1, inend,\
# postorder, postend-right_size, postend-1)
#
# return root
有了前一题的铺垫,这道题很快就解决了,无非就是 rootVal
变成了最后一个元素,再改改递归函数的参数而已,只要明白二叉树的特性,也不难写出来
889. 根据前序和后序遍历构造二叉树
给定两个整数数组,preorder
和 postorder
,其中 preorder
是一个具有 无重复 值的二叉树的前序遍历,postorder
是同一棵树的后序遍历,重构并返回二叉树。
如果存在多个答案,您可以返回其中 任何 一个。
示例 1:
输入:preorder = [1,2,4,5,3,6,7], postorder = [4,5,2,6,7,3,1] 输出:[1,2,3,4,5,6,7]
示例 2:
输入: preorder = [1], postorder = [1] 输出: [1]
提示:
1 <= preorder.length <= 30
1 <= preorder[i] <= preorder.length
preorder
中所有值都 不同postorder.length == preorder.length
1 <= postorder[i] <= postorder.length
postorder
中所有值都 不同- 保证
preorder
和postorder
是同一棵二叉树的前序遍历和后序遍历
【思路】
这道题和前两道题有一个本质的区别:
通过前序中序,或者后序中序遍历结果可以确定唯一一棵原始二叉树,但是通过前序后序遍历结果无法确定唯一的原始二叉树。
题目也说了,如果有多种可能的还原结果,你可以返回任意一种。
为什么呢?我们说过,构建二叉树的套路很简单,先找到根节点,然后找到并递归构造左右子树即可。
前两道题,可以通过前序或者后序遍历结果找到根节点,然后根据中序遍历结果确定左右子树(题目说了树中没有 val
相同的节点)。
这道题,你可以确定根节点,但是无法确切的知道左右子树有哪些节点。
举个例子,比如给你这个输入:
preorder = [1,2,3], postorder = [3,2,1]
下面这两棵树都是符合条件的,但显然它们的结构不同:
不过话说回来,用后序遍历和前序遍历结果还原二叉树,解法逻辑上和前两道题差别不大,也是通过控制左右子树的索引来构建:
1、首先把前序遍历结果的第一个元素或者后序遍历结果的最后一个元素确定为根节点的值。
2、然后把前序遍历结果的第二个元素作为左子树的根节点的值。
3、在后序遍历结果中寻找左子树根节点的值,从而确定了左子树的索引边界,进而确定右子树的索引边界,递归构造左右子树即可。
【Python 解法】
class Solution:
# 存储 postorder 中值到索引的映射
valToIndex = dict()
def constructFromPrePost(self, preorder, postorder):
for i in range(len(postorder)):
self.valToIndex[postorder[i]] = i
return self.build(preorder, 0, len(preorder) - 1,
postorder, 0, len(postorder) - 1)
# 定义:根据 preorder[preStart..preEnd] 和 postorder[postStart..postEnd]
# 构建二叉树,并返回根节点。
def build(self, preorder, preStart, preEnd,
postorder, postStart, postEnd):
if preStart > preEnd:
return None
if preStart == preEnd:
return TreeNode(preorder[preStart])
# root 节点对应的值就是前序遍历数组的第一个元素
rootVal = preorder[preStart]
# root.left 的值是前序遍历第二个元素
# 通过前序和后序遍历构造二叉树的关键在于通过左子树的根节点
# 确定 preorder 和 postorder 中左右子树的元素区间
leftRootVal = preorder[preStart + 1]
# leftRootVal 在后序遍历数组中的索引
index = self.valToIndex[leftRootVal]
# 左子树的元素个数
leftSize = index - postStart + 1
# 先构造出当前根节点
root = TreeNode(rootVal)
# 递归构造左右子树
# 根据左子树的根节点索引和元素个数推导左右子树的索引边界
root.left = self.build(preorder, preStart + 1, preStart + leftSize,
postorder, postStart, index)
root.right = self.build(preorder, preStart + leftSize + 1, preEnd,
postorder, index + 1, postEnd - 1)
return root
代码和前两道题非常类似,我们可以看着代码思考一下,为什么通过前序遍历和后序遍历结果还原的二叉树可能不唯一呢?
关键在这一句:
int leftRootVal = preorder[preStart + 1];
我们假设前序遍历的第二个元素是左子树的根节点,但实际上左子树有可能是空指针,那么这个元素就应该是右子树的根节点。由于这里无法确切进行判断,所以导致了最终答案的不唯一。
至此,通过前序和后序遍历结果还原二叉树的问题也解决了。
最后呼应下前文,二叉树的构造问题一般都是使用「分解问题」的思路:构造整棵树 = 根节点 + 构造左子树 + 构造右子树。先找出根节点,然后根据根节点的值找到左右子树的元素,进而递归构建出左右子树。