力扣解题-105. 从前序与中序遍历序列构造二叉树
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
示例 1:
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
示例 2:
输入: preorder = [-1], inorder = [-1]
输出: [-1]
提示:
1 <= preorder.length <= 3000
inorder.length == preorder.length
-3000 <= preorder[i], inorder[i] <= 3000
preorder 和 inorder 均 无重复 元素
inorder 均出现在 preorder
preorder 保证 为二叉树的前序遍历序列
inorder 保证 为二叉树的中序遍历序列
Related Topics
树、数组、哈希表、分治、二叉树
示例解答
解题思路
核心方法:分治递归+哈希表优化,利用前序遍历“根→左→右”和中序遍历“左→根→右”的特性,通过分治思想递归构建二叉树,哈希表用于快速定位中序遍历中根节点的位置,将时间复杂度从O(n²)优化到O(n),是本题的最优解法。
核心逻辑拆解
构造二叉树的核心是“先找根,再分左右,递归构建”,具体分为三步:
- 利用前序找根:前序遍历的第一个元素是当前子树的根节点;
- 利用中序分左右:在中序遍历中找到根节点的位置,其左侧是左子树的所有节点,右侧是右子树的所有节点;
- 递归构建子树:根据中序划分的左右子树节点数量,确定前序遍历中左右子树的区间,递归构建左右子树。
详细步骤拆解
1. 预处理:哈希表存储中序节点索引
- 遍历中序数组
inorder,将每个节点值和其索引存入哈希表inMap; - 作用:将“查找中序根节点位置”的时间从O(n)降至O(1),避免每次递归都遍历中序数组。
2. 递归函数参数说明
build(preorder, preStart, preEnd, inorder, inStart, inEnd, inMap)的参数含义:
preStart/preEnd:当前子树在前序数组中的区间;inStart/inEnd:当前子树在中序数组中的区间;inMap:中序节点值→索引的哈希表。
3. 递归终止条件
当preStart > preEnd或inStart > inEnd时,说明当前子树无节点,返回null(空子树)。
4. 单轮递归核心操作
以示例1 preorder = [3,9,20,15,7]、inorder = [9,3,15,20,7]为例:
- 步骤1:确定根节点:前序区间起始值
preorder[preStart] = 3是根节点; - 步骤2:定位中序根节点:通过
inMap找到3在中序的索引inRoot = 1; - 步骤3:计算左子树节点数:
leftNum = inRoot - inStart = 1 - 0 = 1(左子树有1个节点); - 步骤4:递归构建左子树:
- 前序区间:
[preStart+1, preStart+leftNum] = [1,1](对应值9); - 中序区间:
[inStart, inRoot-1] = [0,0](对应值9);
- 前序区间:
- 步骤5:递归构建右子树:
- 前序区间:
[preStart+leftNum+1, preEnd] = [2,4](对应值20、15、7); - 中序区间:
[inRoot+1, inEnd] = [2,4](对应值15、20、7);
- 前序区间:
- 步骤6:返回当前根节点:将左右子树挂载到根节点后,返回根节点。
具体执行流程(示例1)
| 递归层级 | 根节点 | 前序区间 | 中序区间 | 左子树节点数 | 操作结果 |
|---|---|---|---|---|---|
| 1 | 3 | [0,4] | [0,4] | 1 | 构建根3,左子树9,右子树20 |
| 2 | 9 | [1,1] | [0,0] | 0 | 构建根9,无左右子树 |
| 2 | 20 | [2,4] | [2,4] | 1 | 构建根20,左子树15,右子树7 |
| 3 | 15 | [3,3] | [2,2] | 0 | 构建根15,无左右子树 |
| 3 | 7 | [4,4] | [4,4] | 0 | 构建根7,无左右子树 |
性能说明
- 时间复杂度:O(n)
- 哈希表预处理:O(n);
- 递归构建:每个节点仅被处理一次,O(n);
- 总复杂度:O(n)(无哈希表优化时为O(n²),因每次找中序根节点需O(n))。
- 空间复杂度:O(n)
- 哈希表存储n个节点:O(n);
- 递归栈深度:最坏情况(斜树)O(n),最好情况(平衡树)O(logn);
- 总复杂度:O(n)。
- 优势:
- 分治思想贴合二叉树的递归结构,逻辑清晰;
- 哈希表优化大幅提升效率,适合大数据量场景;
- 天然处理单节点、空树等边界场景。
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder==null || inorder==null || preorder.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(preorder, 0, preorder.length - 1,
inorder, 0, inorder.length - 1,
inMap);
}
public TreeNode build(int[] preorder, int preStart, int preEnd,
int[] inorder, int inStart, int inEnd,
Map<Integer,Integer> inMap){
//递归终止条件,区间无效 → 空子树
if (preStart > preEnd || inStart > inEnd) {
return null;
}
//前序队列中的第一个元素是当前树的根
int root=preorder[preStart];
//构建树,使用根节点
TreeNode rootTree=new TreeNode(root);
//在中序中找到根的位置
int inRoot=inMap.get(root);
//算出左节点的数量
int leftNum=inRoot-inStart;
// 4. 递归构建左子树
// 前序范围:[preStart+1, preStart+leftSize]
// 中序范围:[inStart, inRootIndex-1]
rootTree.left = build(preorder, preStart + 1, preStart + leftNum,
inorder, inStart, inRoot - 1,
inMap);
// 5. 递归构建右子树
// 前序范围:[preStart+leftSize+1, preEnd]
// 中序范围:[inRootIndex+1, inEnd]
rootTree.right = build(preorder, preStart + leftNum + 1, preEnd,
inorder, inRoot + 1, inEnd,
inMap);
return rootTree;
}
拓展解法:迭代法(思路拓展)
核心方法:栈模拟递归过程,利用栈记录待构建右子树的节点,结合前序和中序的遍历特性迭代构建二叉树,空间复杂度仍为O(n),但避免了递归栈的调用,适合理解非递归构建二叉树的逻辑。
代码实现
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder == null || preorder.length == 0) {
return null;
}
// 栈存储待构建右子树的节点
Stack<TreeNode> stack = new Stack<>();
// 前序第一个元素是根节点
TreeNode root = new TreeNode(preorder[0]);
stack.push(root);
// 中序遍历的指针,初始指向第一个元素
int inIndex = 0;
// 遍历前序数组(从第二个元素开始)
for (int i = 1; i < preorder.length; i++) {
int preVal = preorder[i];
TreeNode currNode = stack.peek();
// 情况1:当前节点是栈顶节点的左子树
if (currNode.val != inorder[inIndex]) {
currNode.left = new TreeNode(preVal);
stack.push(currNode.left);
}
// 情况2:找到栈顶节点的右子树
else {
// 弹出栈顶,直到找到右子树的父节点
while (!stack.isEmpty() && stack.peek().val == inorder[inIndex]) {
currNode = stack.pop();
inIndex++;
}
// 构建右子树
currNode.right = new TreeNode(preVal);
stack.push(currNode.right);
}
}
return root;
}
核心逻辑说明
- 初始化:根节点入栈,中序指针
inIndex初始为0; - 遍历前序数组:
- 若当前栈顶节点值≠中序
inIndex对应值:当前节点是栈顶节点的左子树,入栈; - 若相等:弹出栈顶节点(说明其左子树已构建完成),直到找到右子树的父节点,构建右子树并入栈;
- 若当前栈顶节点值≠中序
- 核心原理:中序遍历的指针
inIndex标记了“已构建完成左子树的最后一个节点”,当栈顶节点值等于该值时,说明其左子树已构建完毕,接下来需构建右子树。
性能说明
- 时间复杂度:O(n)(每个节点仅入栈/出栈一次);
- 空间复杂度:O(n)(栈最多存储n个节点);
- 优势:非递归实现,避免递归栈溢出风险;
- 劣势:逻辑较递归法复杂,不易理解和调试。
总结
- 分治递归+哈希表优化(最优解):O(n)时间+O(n)空间,逻辑清晰、效率最优,是工程中首选的解法;
- 迭代法(栈模拟):O(n)时间+O(n)空间,非递归实现,适合拓展思路;
- 关键技巧:
- 核心思想:前序定根、中序分左右,分治递归构建二叉树;
- 效率优化:哈希表是降低时间复杂度的关键,避免重复遍历中序数组;
- 边界处理:递归终止条件(区间无效返回null)天然覆盖空树、单节点树等场景;
- 核心前提:前序和中序数组无重复元素,保证哈希表定位的唯一性。