# 「力扣」第 106 题:从中序与后序遍历序列构造二叉树(中等)
# 题目描述
Given two integer arrays inorder
and postorder
where inorder
is the inorder traversal of a binary tree and postorder
is the postorder traversal of the same tree, construct and return the binary tree.
Example 1:
Input: inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]
Output: [3,9,20,null,null,15,7]
Example 2:
Input: inorder = [-1], postorder = [-1]
Output: [-1]
Constraints:
1 <= inorder.length <= 3000
postorder.length == inorder.length
-3000 <= inorder[i], postorder[i] <= 3000
inorder
andpostorder
consist of unique values.- Each value of
postorder
also appears ininorder
. inorder
is guaranteed to be the inorder traversal of the tree.postorder
is guaranteed to be the postorder traversal of the tree.
# 一句话题解(可以只看这里,然后直接跳到「参考代码 3」)
- 二叉树后序遍历的最后一个元素一定是根结点;
- 二叉树的根结点把中序遍历序列分成两个区间:
- 左边区间的 所有 结点构成根结点的左子树;
- 右边区间的 所有 结点构成根结点的右子树。
递归构建二叉树即可,注意题目当中说「inorder
和 postorder
都由 不同 的值组成」,这一点很重要:
- 保证了构建的树是唯一的;
- 可以使用「哈希表」保存「数值」与「下标」的关系,优化时间复杂度。
大家可以直接看「方法二」和「参考代码 3」,之前的描述展示了思考的过程,不是最优解。
大家可以直接看「方法二」和「参考代码 3」,之前的描述展示了思考的过程,不是最优解。
大家可以直接看「方法二」和「参考代码 3」,之前的描述展示了思考的过程,不是最优解。
# 思路分析
解决二叉树的问题通常会用到分治思想,分治思想一般通过递归方法实现。
复习分治法的思想:把原问题分解(Divide)成若干个与原问题结构相同但规模更小的子问题,待子问题解决(Conquer)以后,再合并(Combine)它们,原问题就得以解决。
归并排序和快速排序是典型的分治法思想的应用,其中:
- 归并排序先无脑地「分」,在「合」的时候就麻烦一些;
- 快速排序在 partition 上花了很多时间,即在「分」上花了很多时间,然后就递归处理下去就好了,没有在「合」的过程。
以题目中给出的例子为例,讲解如何构建二叉树。
中序遍历
inorder = [9,3,15,20,7]
后序遍历postorder = [9,15,7,20,3]
图画完以后才发现这个例子不太好,数组长度多一些就能把思路展现得更清楚了。大家可以参考我在「力扣」第 105 题写的题解 《分治法(Python 代码、Java 代码)》 (opens new window),两道问题的解法是一样的。
注意:这道问题其实并不难,我们在草稿纸上写写画画,就能把思路想清楚,但是在编码上会有一些小陷阱,在计算索引边界值要认真一些。
下面给出两种写法,区别在于空间复杂度:
# 方法一:在递归方法中,传入数组的拷贝(不推荐、复杂度较高)
该方法在计算索引的时候会稍微容易一些。
参考代码 1:
Java 代码:
import java.util.Arrays;
public class Solution {
/**
* @param inorder 中序遍历序列
* @param postorder 后序遍历序列
* @return
*/
public TreeNode buildTree(int[] inorder, int[] postorder) {
int inlen = inorder.length;
int postlen = postorder.length;
assert inlen == postlen;
if (inlen == 0) {
return null;
}
if (inlen == 1) {
return new TreeNode(inorder[0]);
}
// 后序遍历的最后一个结点就是根结点
int rootVal = postorder[postlen - 1];
// 在中序遍历中找到根结点的索引,得到左右子树的一个划分
int dividePoint = 0;
for (int i = 0; i < inlen; i++) {
if (inorder[i] == rootVal) {
dividePoint = i;
break;
}
}
TreeNode rootNode = new TreeNode(rootVal);
// Arrays.copyOfRange() 方法的第 1 个参数是源数组
// 第 2 个参数是源数组的起始位置(可以取到)
// 第 3 个参数是源数组的起始位置(不可以取到)
// 这里复制了数组,使用了一些空间,因此空间复杂度是 O(N)
rootNode.left = buildTree(Arrays.copyOfRange(inorder, 0, dividePoint), Arrays.copyOfRange(postorder, 0, dividePoint));
rootNode.right = buildTree(Arrays.copyOfRange(inorder, dividePoint + 1, inlen), Arrays.copyOfRange(postorder, dividePoint, postlen - 1));
return rootNode;
}
}
Python 代码:
from typing import List
class Solution:
def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode:
assert len(inorder) == len(postorder)
if len(inorder) == 0:
return None
if len(inorder) == 1:
# 这里要返回结点,而不是返回具体的数
return TreeNode(inorder[0])
# 后序遍历的最后一个结点就是根结点
root = TreeNode(postorder[-1])
# 在中序遍历中找到根结点的索引,得到左右子树的一个划分
pos = inorder.index(postorder[-1])
# 这里的列表切片使用的是复制值,使用了一些空间,因此空间复杂度是 O(N)
root.left = self.buildTree(inorder[:pos], postorder[:pos])
root.right = self.buildTree(inorder[pos + 1:], postorder[pos:-1])
return root
复杂度分析:
- 时间复杂度:
,这里 是二叉树的结点个数,算法中每个结点都会被看到一次,而查找根结点在中序遍历序列中的位置,又需要遍历一次,递归的深度是对数级别的,因此时间复杂度是 。 - 空间复杂度:
,构造一棵树需要 个结点。
# 方法二:在递归方法中,传入子数组的边界下标
注意:在递归方法中,有一个数组的边界下标。
计算的依据是递归方法传入的 中序遍历数组(的子数组)和后序遍历数组(的子数组)的长度相等。我的办法是解方程计算未知数,具体需要计算哪个参数我在下面的代码中已经注明了。
下面展示了一个计算边界的方法。
参考代码 2:
Java 代码:
public class Solution {
public TreeNode buildTree(int[] inorder, int[] postorder) {
int inLen = inorder.length;
int postLen = postorder.length;
// 特判
if (inLen != postLen) {
throw new RuntimeException("输入错误");
}
return buildTree(inorder, 0, inLen - 1, postorder, 0, postLen - 1);
}
/**
* 使用中序遍历序列 inorder 的子区间 [inLeft, inRight]
* 与后序遍历序列 postorder 的子区间 [postLeft, postRight] 构建二叉树
*
* @param inorder 中序遍历序列
* @param inLeft 中序遍历序列的左边界
* @param inRight 中序遍历序列的右边界
* @param postorder 后序遍历序列
* @param postLeft 后序遍历序列的左边界
* @param postRight 后序遍历序列的右边界
* @return 二叉树的根结点
*/
private TreeNode buildTree(int[] inorder, int inLeft, int inRight,
int[] postorder, int postLeft, int postRight) {
if (inLeft > inRight || postLeft > postRight) {
return null;
}
int pivot = postorder[postRight];
int pivotIndex = inLeft;
// 注意这里如果编写不当,有数组下标越界的风险
while (inorder[pivotIndex] != pivot) {
pivotIndex++;
}
TreeNode root = new TreeNode(pivot);
root.left = buildTree(inorder, inLeft, pivotIndex - 1,
postorder, postLeft, postRight - inRight + pivotIndex - 1);
root.right = buildTree(inorder, pivotIndex + 1, inRight,
postorder, postRight - inRight + pivotIndex, postRight - 1);
return root;
}
}
Python 代码:
from typing import List
class Solution:
def __init__(self):
self.inorder = None
self.postorder = None
def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode:
assert len(inorder) == len(postorder)
size = len(inorder)
self.inorder = inorder
self.postorder = postorder
return self.__dfs(0, size - 1, 0, size - 1)
def __dfs(self, in_l, in_r, post_l, post_r):
if in_l > in_r or post_l > post_r:
return None
val = self.postorder[post_r]
# 后序遍历的最后一个结点就是根结点
root = TreeNode(val)
# 在中序遍历中找到根结点的索引,得到左右子树的一个划分
pos = self.inorder.index(val)
# 注意:第 4 个参数是计算出来的,依据:两边区间长度相等
root.left = self.__dfs(in_l, pos - 1, post_l, pos - 1 - in_l + post_l)
# 注意:第 3 个参数是计算出来的,依据:两边区间长度相等
root.right = self.__dfs(pos + 1, in_r, post_r - in_r + pos, post_r - 1)
return root
复杂度分析:(同参考代码 1)
可以在递归构造前,把中序遍历的值和下标放在哈希表中,就不需要通过遍历得到当前根结点在中序遍历中的位置了。
参考代码 3:
Java 代码:
import java.util.HashMap;
import java.util.Map;
public class Solution {
/**
* 让 postorder 成为全局变量,以免在递归方法中一直传递
*/
private int[] postorder;
private Map<Integer, Integer> hash;
public TreeNode buildTree(int[] inorder, int[] postorder) {
int inLen = inorder.length;
int postLen = postorder.length;
if (inLen != postLen) {
throw new RuntimeException("输入错误");
}
this.postorder = postorder;
hash = new HashMap<>();
for (int i = 0; i < inLen; i++) {
hash.put(inorder[i], i);
}
return buildTree(0, inLen - 1, 0, postLen - 1);
}
/**
* 使用中序遍历序列 inorder 的子区间 [inLeft, inRight]
* 与后序遍历序列 postorder 的子区间 [postLeft, postRight] 构建二叉树
*
* @param inLeft 中序遍历序列的左边界
* @param inRight 中序遍历序列的右边界
* @param postLeft 后序遍历序列的左边界
* @param postRight 后序遍历序列的右边界
* @return 二叉树的根结点
*/
private TreeNode buildTree(int inLeft, int inRight, int postLeft, int postRight) {
if (inLeft > inRight || postLeft > postRight) {
return null;
}
int pivot = postorder[postRight];
int pivotIndex = hash.get(pivot);
TreeNode root = new TreeNode(pivot);
root.left = buildTree(inLeft, pivotIndex - 1, postLeft, postRight - inRight + pivotIndex - 1);
root.right = buildTree(pivotIndex + 1, inRight, postRight - inRight + pivotIndex, postRight - 1);
return root;
}
}
复杂度分析:
- 时间复杂度:
,这里 是二叉树的结点个数,二叉树的每个结点都遍历了 次,第 1 次是扫描构建哈希表,第 2 次是递归构建二叉树; - 空间复杂度:
。
作者:liweiwei1419 链接:https://suanfa8.com/tree/solutions/0106-construct-binary-tree-from-inorder-and-postorder-traversal 来源:算法吧 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。