在二叉树的算法题型中,“根据遍历序列构造二叉树”是经典考点,而 LeetCode 105 题——从前序与中序遍历序列构造二叉树,更是这一考点的核心代表。这道题不仅能考察我们对二叉树遍历规则的理解,还能检验递归思维和哈希表优化的应用,今天就来一步步拆解这道题,从思路到代码,吃透每一个细节。
一、题目回顾
题目给出两个整数数组preorder 和 inorder,其中 preorder 是二叉树的先序遍历序列,inorder 是同一棵二叉树的中序遍历序列,要求我们构造这棵二叉树并返回其根节点。
补充基础:二叉树遍历规则
-
先序遍历(preorder):根节点 → 左子树 → 右子树(根在前,左右在后);
-
中序遍历(inorder):左子树 → 根节点 → 右子树(根在中间,左右分居两侧)。
核心关键:先序遍历的第一个元素一定是二叉树的根节点;而中序遍历中,根节点左侧的所有元素是左子树的中序序列,右侧的所有元素是右子树的中序序列。利用这两个特性,我们就能递归地构造出整个二叉树。
二、解题思路(核心逻辑)
这道题的解题核心是「递归分治」,配合哈希表优化查找效率,具体思路可以分为 4 步,我们结合例子来理解(假设 preorder = [3,9,20,15,7],inorder = [9,3,15,20,7]):
步骤1:确定根节点
根据先序遍历规则,preorder 的第一个元素 preorder[0] 就是整个二叉树的根节点(例子中根节点是 3)。
步骤2:划分左右子树的中序序列
在中序序列 inorder 中找到根节点的索引(例子中 3 的索引是 1):
-
根节点左侧的元素([9]):左子树的中序序列;
-
根节点右侧的元素([15,20,7]):右子树的中序序列。
步骤3:划分左右子树的先序序列
左右子树的节点数量,在中序序列和先序序列中是一致的:
-
左子树的节点数 = 根节点在中序的索引 - 中序序列的起始索引(例子中 1 - 0 = 1,左子树有 1 个节点);
-
因此,先序序列中,根节点之后的「左子树节点数」个元素([9])是左子树的先序序列;
-
剩下的元素([20,15,7])是右子树的先序序列。
步骤4:递归构造左右子树
对左子树和右子树,重复上述 3 个步骤:找到子树的根节点、划分左右子序列,直到序列为空(递归终止条件),最终拼接出整个二叉树。
优化点:哈希表加速查找
如果每次在中序序列中查找根节点都用遍历的方式,时间复杂度会达到 O(n²)(n 是节点总数)。我们可以提前用哈希表(Map)存储中序序列中「元素 → 索引」的映射,这样每次查找根节点的索引只需 O(1) 时间,整体时间复杂度优化到 O(n)。
三、完整代码实现(TypeScript)
先给出 TreeNode 类的定义(题目已提供,此处复用并补充注释),再实现核心的 buildTree 函数,每一步代码都附上详细注释,方便理解:
// 二叉树节点类定义
class TreeNode {
val: number
left: TreeNode | null // 左子节点,默认为null
right: TreeNode | null // 右子节点,默认为null
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = (val === undefined ? 0 : val) // 节点值,默认0
this.left = (left === undefined ? null : left)
this.right = (right === undefined ? null : right)
}
}
function buildTree(preorder: number[], inorder: number[]): TreeNode | null {
// 1. 构建中序序列的哈希映射,key是节点值,value是对应索引
const map = new Map<number, number>();
inorder.forEach((val, index) => {
map.set(val, index);
});
/**
* 递归辅助函数:构造当前范围内的二叉树
* @param preorderStart 先序序列当前范围的起始索引
* @param preorderEnd 先序序列当前范围的结束索引
* @param inorderStart 中序序列当前范围的起始索引
* @param inorderEnd 中序序列当前范围的结束索引
* @returns 当前范围的二叉树根节点
*/
const helper = (
preorderStart: number,
preorderEnd: number,
inorderStart: number,
inorderEnd: number
): TreeNode | null => {
// 递归终止条件:当前范围无节点(起始索引>结束索引),返回null
if (preorderStart > preorderEnd || inorderStart > inorderEnd) {
return null;
}
// 2. 确定当前范围的根节点(先序序列的第一个元素)
const rootVal = preorder[preorderStart];
const root = new TreeNode(rootVal); // 创建根节点
// 3. 找到根节点在中序序列中的索引,用于划分左右子树
const inorderIndex = map.get(rootVal)!; // !表示非null断言(确保能找到索引)
const leftSize = inorderIndex - inorderStart; // 左子树的节点数
// 4. 递归构造左子树
// 左子树先序范围:preorderStart+1 ~ preorderStart+leftSize(根节点后leftSize个元素)
// 左子树中序范围:inorderStart ~ inorderIndex-1(根节点左侧元素)
root.left = helper(
preorderStart + 1,
preorderStart + leftSize,
inorderStart,
inorderIndex - 1
);
// 5. 递归构造右子树
// 右子树先序范围:preorderStart+leftSize+1 ~ preorderEnd(左子树之后的剩余元素)
// 右子树中序范围:inorderIndex+1 ~ inorderEnd(根节点右侧元素)
root.right = helper(
preorderStart + leftSize + 1,
preorderEnd,
inorderIndex + 1,
inorderEnd
);
return root; // 返回当前范围的根节点
}
// 初始调用递归函数,范围是整个先序和中序序列
return helper(0, preorder.length - 1, 0, inorder.length - 1);
};
四、代码关键细节解析
1. 递归终止条件
当 preorderStart > preorderEnd 或 inorderStart > inorderEnd 时,说明当前范围内没有节点,返回 null(比如叶子节点的左右子节点,就会触发这个条件)。
2. 左子树节点数的计算
leftSize = inorderIndex - inorderStart,这个计算是划分先序序列的关键——因为先序序列中,根节点之后的 leftSize 个元素,必然是左子树的先序序列,剩下的就是右子树的先序序列。
3. 哈希表的非null断言
代码中 map.get(rootVal)! 用到了 TypeScript 的非null断言(!),原因是题目保证了 preorder 和 inorder 是同一棵二叉树的遍历序列,因此根节点的值一定能在中序序列中找到,不会返回 null。
4. 时间和空间复杂度
-
时间复杂度:O(n),n 是节点总数。哈希表构建需要 O(n) 时间,递归过程中每个节点被处理一次,每次查找根节点索引是 O(1);
-
空间复杂度:O(n),哈希表存储 n 个元素,递归调用栈的深度最坏情况下是 O(n)(比如斜树),最好情况下是 O(log n)(平衡二叉树)。
五、常见易错点提醒
-
先序序列的划分错误:容易把右子树的先序起始索引算错,记住是
preorderStart + leftSize + 1(跳过根节点和左子树); -
中序序列的边界错误:左子树的中序结束索引是
inorderIndex - 1,右子树的中序起始索引是inorderIndex + 1,容易漏写 ±1 导致死循环; -
忽略空数组情况:当 preorder 和 inorder 为空时,直接返回 null(递归终止条件会处理,但需注意初始调用时的边界);
-
不用哈希表优化:直接遍历中序序列找根节点,会导致时间复杂度飙升,在 n 较大时(比如 10^4 级别)会超时。
六、总结
LeetCode 105 题的核心是「利用遍历序列的特性 + 递归分治 + 哈希表优化」,解题的关键在于抓住“先序定根、中序分左右”的规律,再通过递归逐步构造子树。
这道题不仅能帮我们巩固二叉树的遍历知识,还能锻炼递归思维——递归的本质就是“把大问题拆成小问题,解决小问题后拼接结果”,这里的大问题是“构造整个二叉树”,小问题是“构造左子树”和“构造右子树”。
如果能吃透这道题,再遇到“从中序与后序遍历构造二叉树”(LeetCode 106 题)就会事半功倍,因为思路完全相通,只是根节点的位置和序列划分方式略有不同。