力扣解题-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 保证是树的后序遍历
Related Topics
树、数组、哈希表、分治、二叉树
示例解答
解题思路
核心方法:分治递归+哈希表优化,利用后序遍历“左→右→根”和中序遍历“左→根→右”的核心特性,通过分治思想将构建整棵树拆解为“构建根节点→递归构建左右子树”,哈希表用于快速定位中序数组中根节点的位置,将时间复杂度从O(n²)优化至O(n),是本题的最优解法。
核心逻辑拆解
构造二叉树的关键在于“双序列联动找边界”,核心三步:
- 找根节点:后序遍历的最后一个元素一定是当前子树的根节点(后序遍历的“根”在末尾);
- 分左右子树:在中序数组中找到根节点的位置,其左侧所有元素是左子树节点,右侧所有元素是右子树节点;
- 递归构建:根据中序数组划分的左右子树节点数量,确定后序数组中左右子树的区间,递归构建左右子树并挂载到根节点。
详细步骤拆解
1. 预处理:哈希表映射中序值与索引
- 遍历中序数组
inorder,将每个节点值作为key、索引作为value存入inMap; - 核心作用:将“在中序数组中查找根节点位置”的操作从O(n)(遍历)降至O(1)(哈希表查询),这是优化时间复杂度的关键。
2. 递归函数参数说明
build(inorder, inStart, inEnd, postorder, postStart, postEnd, inMap)的参数含义:
inStart/inEnd:当前子树在中序数组中的起止索引;postStart/postEnd:当前子树在后序数组中的起止索引;inMap:中序值→索引的哈希表,避免重复遍历。
3. 递归终止条件
当inStart > inEnd或postStart > postEnd时,说明当前子树无节点(空区间),返回null,这是递归的基线条件,天然处理“叶子节点的子节点为空”的场景。
4. 单轮递归核心操作(以示例1为例)
输入:inorder = [9,3,15,20,7],postorder = [9,15,7,20,3]
- 步骤1:确定根节点:后序末尾值
postorder[4] = 3是整棵树的根; - 步骤2:定位中序根节点:通过
inMap找到3在中序的索引inRootIndex = 1; - 步骤3:计算左子树节点数:
leftSize = 1 - 0 = 1(中序根左侧有1个节点); - 步骤4:递归构建左子树:
- 中序区间:
[0, 0](仅9),后序区间:[0, 0](仅9);
- 中序区间:
- 步骤5:递归构建右子树:
- 中序区间:
[2, 4](15、20、7),后序区间:[1, 3](15、7、20);
- 中序区间:
- 步骤6:挂载并返回:将左右子树挂载到根节点3,返回根节点。
关键区间划分逻辑(核心易错点)
| 子树类型 | 中序区间 | 后序区间 | 推导依据 |
|---|---|---|---|
| 左子树 | [inStart, inRootIndex-1] | [postStart, postStart + leftSize - 1] | 左子树节点数=leftSize |
| 右子树 | [inRootIndex+1, inEnd] | [postStart + leftSize, postEnd - 1] | 后序末尾是根,需排除根节点 |
执行流程可视化(示例1)
| 递归层级 | 根节点 | 中序区间 | 后序区间 | 左子树节点数 | 操作结果 |
|---|---|---|---|---|---|
| 1 | 3 | [0,4] | [0,4] | 1 | 构建根3,左9、右20 |
| 2 | 9 | [0,0] | [0,0] | 0 | 构建根9,无左右子树 |
| 2 | 20 | [2,4] | [1,3] | 1 | 构建根20,左15、右7 |
| 3 | 15 | [2,2] | [1,1] | 0 | 构建根15,无左右子树 |
| 3 | 7 | [4,4] | [2,2] | 0 | 构建根7,无左右子树 |
性能说明
- 时间复杂度:O(n)
- 哈希表构建:O(n);
- 递归构建:每个节点仅被处理一次,O(n);
- 无哈希表优化时为O(n²)(每次找根需遍历中序数组)。
- 空间复杂度:O(n)
- 哈希表存储n个节点:O(n);
- 递归栈深度:平衡树O(logn),斜树O(n);
- 总复杂度由哈希表主导,为O(n)。
- 优势:
- 分治思想贴合二叉树的递归结构,逻辑清晰易维护;
- 哈希表优化后效率最优,适合题目中n≤3000的场景;
- 天然处理单节点、空树等边界条件。
public TreeNode buildTree(int[] inorder, int[] postorder) {
if (inorder == null || postorder == null || inorder.length == 0) {
return null;
}
// 构建中序值到索引的映射
Map<Integer, Integer> inMap = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
inMap.put(inorder[i], i);
}
return build(inorder, 0, inorder.length - 1,
postorder, 0, postorder.length - 1,
inMap);
}
private TreeNode build(int[] inorder, int inStart, int inEnd,
int[] postorder, int postStart, int postEnd,
Map<Integer, Integer> inMap) {
// 递归终止条件
if (inStart > inEnd || postStart > postEnd) {
return null;
}
// 1. 后序的最后一个元素是当前子树的根
int rootVal = postorder[postEnd];
TreeNode root = new TreeNode(rootVal);
// 2. 在中序中找到根的位置
int inRootIndex = inMap.get(rootVal);
// 3. 计算左子树的节点数量
int leftSize = inRootIndex - inStart;
// 4. 递归构建左子树
// 中序: [inStart, inRootIndex - 1]
// 后序: [postStart, postStart + leftSize - 1]
root.left = build(inorder, inStart, inRootIndex - 1,
postorder, postStart, postStart + leftSize - 1,
inMap);
// 5. 递归构建右子树
// 中序: [inRootIndex + 1, inEnd]
// 后序: [postStart + leftSize, postEnd - 1]
root.right = build(inorder, inRootIndex + 1, inEnd,
postorder, postStart + leftSize, postEnd - 1,
inMap);
return root;
}
拓展解法:迭代法(思路拓展)
核心方法:栈模拟递归+逆序遍历,利用后序逆序(根→右→左)与中序逆序(右→根→左)的特性,通过栈记录待构建左子树的节点,迭代完成二叉树构建,空间复杂度仍为O(n),但避免了递归栈的调用。
代码实现
public TreeNode buildTree(int[] inorder, int[] postorder) {
if (postorder == null || postorder.length == 0) {
return null;
}
Stack<TreeNode> stack = new Stack<>();
// 后序最后一个元素是根节点
TreeNode root = new TreeNode(postorder[postorder.length - 1]);
stack.push(root);
int postIdx = postorder.length - 2; // 后序指针从倒数第二个开始
int inIdx = inorder.length - 1; // 中序指针从最后一个开始
while (postIdx >= 0) {
TreeNode curr = stack.peek();
// 情况1:当前节点是栈顶的右子树
if (curr.val != inorder[inIdx]) {
TreeNode right = new TreeNode(postorder[postIdx]);
curr.right = right;
stack.push(right);
postIdx--;
}
// 情况2:找到栈顶节点的左子树
else {
// 弹出已构建完右子树的节点
while (!stack.isEmpty() && stack.peek().val == inorder[inIdx]) {
curr = stack.pop();
inIdx--;
}
// 构建左子树
TreeNode left = new TreeNode(postorder[postIdx]);
curr.left = left;
stack.push(left);
postIdx--;
}
}
return root;
}
核心逻辑说明
- 初始化:根节点入栈,后序指针从倒数第二个元素逆序遍历,中序指针从末尾逆序遍历;
- 构建右子树:若栈顶节点值≠中序指针值,当前后序值是栈顶的右子节点,入栈;
- 切换到左子树:若栈顶节点值=中序指针值,说明栈顶的右子树已构建完成,弹出栈顶并左移中序指针,直到找到左子树的父节点;
- 构建左子树:将当前后序值作为父节点的左子节点,入栈。
性能说明
- 时间复杂度:O(n)(每个节点仅入栈/出栈一次);
- 空间复杂度:O(n)(栈最多存储n个节点);
- 适用场景:适合需要避免递归栈溢出的极端场景(如斜树),但逻辑较递归法复杂,可读性较低。
总结
- 分治递归+哈希表(最优解):O(n)时间+O(n)空间,逻辑清晰、效率最优,工程中首选;
- 迭代法(栈模拟):O(n)时间+O(n)空间,非递归实现,适合拓展思路;
- 核心关键点:
- 后序末尾是根,中序根的左右划分左右子树;
- 哈希表是优化时间复杂度的核心,避免重复遍历中序数组;
- 后序区间划分是易错点,左子树
[postStart, postStart+leftSize-1],右子树[postStart+leftSize, postEnd-1]。