力扣解题-105. 从前序与中序遍历序列构造二叉树

5 阅读7分钟

力扣解题-105. 从前序与中序遍历序列构造二叉树

给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

示例 1:

image.png

输入: 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. 利用前序找根:前序遍历的第一个元素是当前子树的根节点;
  2. 利用中序分左右:在中序遍历中找到根节点的位置,其左侧是左子树的所有节点,右侧是右子树的所有节点;
  3. 递归构建子树:根据中序划分的左右子树节点数量,确定前序遍历中左右子树的区间,递归构建左右子树。
详细步骤拆解
1. 预处理:哈希表存储中序节点索引
  • 遍历中序数组inorder,将每个节点值和其索引存入哈希表inMap
  • 作用:将“查找中序根节点位置”的时间从O(n)降至O(1),避免每次递归都遍历中序数组。
2. 递归函数参数说明

build(preorder, preStart, preEnd, inorder, inStart, inEnd, inMap)的参数含义:

  • preStart/preEnd:当前子树在前序数组中的区间;
  • inStart/inEnd:当前子树在中序数组中的区间;
  • inMap:中序节点值→索引的哈希表。
3. 递归终止条件

preStart > preEndinStart > 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)
递归层级根节点前序区间中序区间左子树节点数操作结果
13[0,4][0,4]1构建根3,左子树9,右子树20
29[1,1][0,0]0构建根9,无左右子树
220[2,4][2,4]1构建根20,左子树15,右子树7
315[3,3][2,2]0构建根15,无左右子树
37[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)。
  • 优势
    1. 分治思想贴合二叉树的递归结构,逻辑清晰;
    2. 哈希表优化大幅提升效率,适合大数据量场景;
    3. 天然处理单节点、空树等边界场景。
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;
}
核心逻辑说明
  1. 初始化:根节点入栈,中序指针inIndex初始为0;
  2. 遍历前序数组
    • 若当前栈顶节点值≠中序inIndex对应值:当前节点是栈顶节点的左子树,入栈;
    • 若相等:弹出栈顶节点(说明其左子树已构建完成),直到找到右子树的父节点,构建右子树并入栈;
  3. 核心原理:中序遍历的指针inIndex标记了“已构建完成左子树的最后一个节点”,当栈顶节点值等于该值时,说明其左子树已构建完毕,接下来需构建右子树。
性能说明
  • 时间复杂度:O(n)(每个节点仅入栈/出栈一次);
  • 空间复杂度:O(n)(栈最多存储n个节点);
  • 优势:非递归实现,避免递归栈溢出风险;
  • 劣势:逻辑较递归法复杂,不易理解和调试。

总结

  1. 分治递归+哈希表优化(最优解):O(n)时间+O(n)空间,逻辑清晰、效率最优,是工程中首选的解法;
  2. 迭代法(栈模拟):O(n)时间+O(n)空间,非递归实现,适合拓展思路;
  3. 关键技巧:
    • 核心思想:前序定根、中序分左右,分治递归构建二叉树;
    • 效率优化:哈希表是降低时间复杂度的关键,避免重复遍历中序数组;
    • 边界处理:递归终止条件(区间无效返回null)天然覆盖空树、单节点树等场景;
    • 核心前提:前序和中序数组无重复元素,保证哈希表定位的唯一性。