在二叉树的算法题中,"根据遍历序列构造二叉树"是高频考点,而 LeetCode 106 题(从中序与后序遍历序列构造二叉树)更是经典中的经典。它不仅考察对二叉树遍历规则的理解,还需要运用分治思想拆解问题,新手容易在"区间划分"上栽坑。今天就带大家一步步拆解这道题,从思路分析到代码实现,再到细节避坑,彻底搞懂如何通过两个遍历序列还原二叉树。
一、题目核心认知
先明确题目要求,避免理解偏差:
-
给定两个整数数组
inorder(中序遍历)和postorder(后序遍历),二者对应同一棵二叉树; -
构造并返回这棵二叉树的根节点;
-
默认输入有效(无重复元素,且两个数组长度一致,能构成合法二叉树)。
关键前提:二叉树遍历规则回顾
要解决这道题,必须牢记中序和后序遍历的核心特点(这是解题的灵魂):
-
中序遍历(左-根-右):先遍历左子树,再访问根节点,最后遍历右子树;
-
后序遍历(左-右-根):先遍历左子树,再遍历右子树,最后访问根节点。
核心突破口:后序遍历的最后一个元素 ,一定是当前二叉树的根节点;而中序遍历中,根节点左侧的所有元素都是左子树的节点,右侧的所有元素都是右子树的节点。
二、解题思路拆解(分治思想)
既然能通过后序找到根节点,通过中序划分左右子树,那我们就可以用「分治」的思路,把大问题拆成小问题,递归求解:
步骤1:找到根节点
取 postorder 的最后一个元素,作为当前二叉树的根节点(root)。
步骤2:划分中序遍历的左右子树区间
在inorder 中找到根节点的索引(记为 rootIndex),则:
-
左子树的中序区间:
[inorderStart, rootIndex - 1](根节点左侧所有元素); -
右子树的中序区间:
[rootIndex + 1, inorderEnd](根节点右侧所有元素)。
步骤3:划分后序遍历的左右子树区间
后序遍历的区间长度和中序遍历一致(因为对应同一棵子树),设左子树的节点个数为 leftSize = rootIndex - inorderStart,则:
-
左子树的后序区间:
[postorderStart, postorderStart + leftSize - 1](左子树节点个数为 leftSize); -
右子树的后序区间:
[postorderStart + leftSize, postorderEnd - 1](去掉最后一个根节点,剩余部分前半为左子树,后半为右子树)。
步骤4:递归构造左右子树
用同样的方法,递归构造左子树和右子树,分别赋值给根节点的 left 和 right 指针。
步骤5:优化索引查询(避免重复遍历)
如果每次在中序数组中找根节点索引都用遍历的方式,时间复杂度会很高(O(n²))。我们可以提前用一个哈希表(Map),存储中序数组中「元素-索引」的映射,这样每次查询根节点索引只需 O(1) 时间,整体时间复杂度优化到 O(n)。
三、完整代码实现(TypeScript)
结合上面的思路,我们来实现代码(题目已给出 TreeNode 类,直接复用即可):
typescript
/**
* Definition for a binary tree node.
* class TreeNode {
* val: number
* left: TreeNode | null
* right: TreeNode | null
* constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
* }
*/
class TreeNode {
val: number
left: TreeNode | null
right: TreeNode | null
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = (val === undefined ? 0 : val)
this.left = (left === undefined ? null : left)
this.right = (right === undefined ? null : right)
}
}
function buildTree(inorder: number[], postorder: number[]): TreeNode | null {
// 提前存储中序遍历的「元素-索引」映射,优化查询效率
const map = new Map<number, number>();
inorder.forEach((val, index) => {
map.set(val, index);
});
// 递归辅助函数:根据区间构造子树
// inorderStart/inorderEnd:当前子树在中序数组中的区间
// postorderStart/postorderEnd:当前子树在后序数组中的区间
const helper = (inorderStart: number, inorderEnd: number, postorderStart: number, postorderEnd: number): TreeNode | null => {
// 递归终止条件:区间不合法(没有节点),返回null
if (inorderStart > inorderEnd || postorderStart > postorderEnd) {
return null;
}
// 1. 找到当前子树的根节点(后序数组的最后一个元素)
const rootVal = postorder[postorderEnd];
const root = new TreeNode(rootVal);
// 2. 找到根节点在中序数组中的索引,划分左右子树区间
const rootIndex = map.get(rootVal)!; // 题目保证输入有效,非null
// 3. 计算左子树的节点个数,用于划分后序数组区间
const leftSize = rootIndex - inorderStart;
// 4. 递归构造左子树和右子树,赋值给根节点
// 左子树:中序[start, rootIndex-1],后序[start, start+leftSize-1]
root.left = helper(inorderStart, rootIndex - 1, postorderStart, postorderStart + leftSize - 1);
// 右子树:中序[rootIndex+1, end],后序[start+leftSize, end-1]
root.right = helper(rootIndex + 1, inorderEnd, postorderStart + leftSize, postorderEnd - 1);
// 返回当前子树的根节点
return root;
}
// 初始调用:区间为两个数组的完整区间
return helper(0, inorder.length - 1, 0, postorder.length - 1);
};
四、代码逐行解析(重点+避坑)
很多新手能看懂思路,但写代码时容易在「区间边界」上出错,这里逐行拆解关键部分,帮大家避坑:
1. 哈希表初始化
typescript
const map = new Map<number, number>();
inorder.forEach((val, index) => {
map.set(val, index);
});
作用:提前缓存中序数组的元素和对应索引,后续每次找根节点索引都能直接通过 map.get(rootVal) 获取,避免重复遍历中序数组,降低时间复杂度。
2. 递归辅助函数(helper)
为什么需要辅助函数?因为我们需要通过「区间边界」来划分左右子树,而主函数的参数只有两个数组,无法直接传递区间信息,所以用 helper 函数封装区间参数,实现递归。
3. 递归终止条件
typescript
if (inorderStart > inorderEnd || postorderStart > postorderEnd) {
return null;
}
避坑点:当区间的起始索引大于结束索引时,说明当前区间没有节点,直接返回 null(比如左子树为空或右子树为空的情况)。比如,根节点的左子树如果没有节点,那么 inorderStart 会等于 rootIndex,此时 inorderStart > rootIndex - 1,触发终止条件,返回 null,正好对应 root.left = null。
4. 根节点创建
typescript
const rootVal = postorder[postorderEnd];
const root = new TreeNode(rootVal);
核心:后序遍历的最后一个元素就是当前子树的根节点,这是整个解题的突破口,必须牢记。
5. 区间划分(最容易出错的地方)
typescript
const leftSize = rootIndex - inorderStart;
// 左子树递归调用
root.left = helper(inorderStart, rootIndex - 1, postorderStart, postorderStart + leftSize - 1);
// 右子树递归调用
root.right = helper(rootIndex + 1, inorderEnd, postorderStart + leftSize, postorderEnd - 1);
避坑解析:
-
leftSize 是左子树的节点个数,由「根节点索引 - 中序起始索引」得到,因为中序左子树区间是 [inorderStart, rootIndex - 1],长度为 rootIndex - inorderStart;
-
后序左子树区间的结束索引 = 起始索引 + 左子树节点个数 - 1(因为区间是闭区间,比如起始索引0,个数2,区间是[0,1]);
-
后序右子树的起始索引 = 左子树结束索引 + 1,结束索引 = 原后序结束索引 - 1(去掉根节点);
-
中序右子树区间直接从 rootIndex + 1 开始,到 inorderEnd 结束即可。
五、复杂度分析
-
时间复杂度:O(n),n 是二叉树的节点个数。哈希表初始化遍历一次中序数组(O(n)),每个节点被递归处理一次(O(n)),每次索引查询 O(1);
-
空间复杂度:O(n),哈希表存储 n 个元素(O(n)),递归调用栈的深度最坏情况下为 n(比如二叉树退化为链表),整体空间复杂度 O(n)。
六、总结与拓展
核心总结
这道题的本质是「利用遍历序列的特点找根节点 + 分治思想拆分左右子树」,关键在于两点:
-
牢记后序最后一个元素是根,中序根节点划分左右子树;
-
精准划分两个数组的左右子树区间,避免边界出错(建议多动手画示例,标注区间)。
拓展思考
这道题和 LeetCode 105 题(从前序与中序遍历序列构造二叉树)思路高度一致,只是根节点的位置和区间划分略有不同:
-
105题(前序+中序):前序的第一个元素是根节点;
-
106题(后序+中序):后序的最后一个元素是根节点。
掌握这两道题,就能轻松应对「遍历序列构造二叉树」的所有同类题型。