题目:105. 从前序与中序遍历序列构造二叉树 | 难度:中等
一、题目描述
给定两个整数数组 preorder 和 inorder,其中 preorder 是二叉树的先序遍历,inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
示例 1
plaintext
输入:preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出:[3,9,20,null,null,15,7]
示例 2
plaintext
输入:preorder = [-1], inorder = [-1]
输出:[-1]
核心遍历规律(解题关键)
- 前序遍历(根 → 左 → 右) :数组第一个元素永远是当前子树的根节点
- 中序遍历(左 → 根 → 右) :根节点左侧是左子树全部节点,右侧是右子树全部节点
利用以上特性,我们就可以不断划分左右子树,递归构建二叉树。
二、解法一:基础递归(数组切片分割,新手友好)
思路
- 前序数组首元素作为根节点;
- 在中序数组找到根节点位置,切分出左、右子树的中序数组;
- 根据左子树节点数量,切分出左、右子树的前序数组;
- 递归构建左子树、右子树。
JavaScript 代码
javascript
运行
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} preorder
* @param {number[]} inorder
* @return {TreeNode}
*/
var buildTree = function(preorder, inorder) {
// 递归终止条件:数组为空,代表空子树
if (!preorder.length) return null;
// 前序第一个元素 = 当前根节点
const rootVal = preorder[0];
const root = new TreeNode(rootVal);
// 找到根节点在中序遍历中的下标
const midIndex = inorder.indexOf(rootVal);
// 递归构建左子树
root.left = buildTree(
preorder.slice(1, midIndex + 1),
inorder.slice(0, midIndex)
);
// 递归构建右子树
root.right = buildTree(
preorder.slice(midIndex + 1),
inorder.slice(midIndex + 1)
);
return root;
};
优缺点
✅ 优点:逻辑直观,代码简短,非常适合初学者理解思路❌ 缺点:
slice不断创建新数组,内存开销大;indexOf线性查找,每次查找根节点需要 O (n);大数据量下效率较差。
三、解法二:哈希表优化递归(面试推荐最优解)
思路优化点
- 不切割数组:改用下标区间
[left, right]划定子树范围,省去数组拷贝; - 哈希 Map 预存映射:提前记录「节点值 → 中序数组下标」,查找根节点下标 O (1);
- 使用指针顺序遍历前序数组,严格遵循「根→左→右」构建顺序。
JavaScript 代码
javascript
运行
var buildTree = function(preorder, inorder) {
// 建立值 -> 中序索引的哈希映射,快速查找
const valToIndex = new Map();
inorder.forEach((val, idx) => {
valToIndex.set(val, idx);
});
// 前序遍历指针,全局共享,依次取根节点
let preIndex = 0;
/**
* @param {number} left 当前子树在中序数组左边界
* @param {number} right 当前子树在中序数组右边界
*/
const dfs = (left, right) => {
if (left > right) return null;
// 取出当前根节点
const rootVal = preorder[preIndex++];
const root = new TreeNode(rootVal);
// 获取根在中序的位置
const mid = valToIndex.get(rootVal);
// 先构建左子树,再构建右子树(契合前序遍历顺序)
root.left = dfs(left, mid - 1);
root.right = dfs(mid + 1, right);
return root;
};
return dfs(0, inorder.length - 1);
};
复杂度分析
- 时间复杂度:
O(n),每个节点只遍历一次,哈希查找 O (1) - 空间复杂度:
O(n),哈希表存储所有节点 + 递归调用栈
⭐ 面试首选写法,兼顾可读性与性能
四、解法三:迭代栈实现(非递归,进阶思路)
递归本质是程序调用栈,我们可以手动维护栈,不用递归函数实现构建。
思路
-
前序第一个元素作为根节点,入栈;设置中序遍历指针;
-
依次遍历后续前序节点:
- 如果栈顶节点值!= 当前中序指针的值:新节点作为栈顶左孩子;
- 如果相等:持续弹出栈,找到最后一个匹配中序值的节点,新节点作为它的右孩子;
-
新节点持续入栈,循环完成整棵树构建。
JavaScript 代码
javascript
运行
var buildTree = function(preorder, inorder) {
if (!preorder.length) return null;
const root = new TreeNode(preorder[0]);
const stack = [root];
let inIndex = 0;
for (let i = 1; i < preorder.length; i++) {
const currVal = preorder[i];
const currNode = new TreeNode(currVal);
let topNode = stack[stack.length - 1];
if (topNode.val !== inorder[inIndex]) {
// 还没遇到中序对应节点,挂左子树
topNode.left = currNode;
} else {
// 不断出栈,寻找可以挂载右孩子的父节点
while (stack.length && stack[stack.length - 1].val === inorder[inIndex]) {
topNode = stack.pop();
inIndex++;
}
topNode.right = currNode;
}
stack.push(currNode);
}
return root;
};
优缺点
✅ 优点:消除递归调用栈溢出风险;考察对二叉树遍历深层次理解❌ 缺点:逻辑绕,代码不易理解,日常业务、面试不做首选
五、三种方案横向对比
表格
| 解法 | 实现方式 | 难度 | 适用场景 |
|---|---|---|---|
| 基础递归切片 | 递归 + slice 分割数组 | ⭐ | 初学理解思路,练习算法逻辑 |
| 哈希优化递归 | 递归 + Map + 下标边界 | ⭐⭐ | 面试答题,最优综合方案 |
| 栈迭代 | 循环 + 手动栈 | ⭐⭐⭐ | 进阶拓展,深挖遍历本质 |