题目描述

✅ 105. 从前序与中序遍历序列构造二叉树

image-20260928191145362

image-20260928191145363

题意分析

preorder 和 inorder 分别是同一棵二叉树的前序、中序遍历结果,需要重新创建这棵树并返回根节点。前序按“根、左子树、右子树”访问,中序按“左子树、根、右子树”访问。

题目保证两个序列合法且节点值互不相同,因此它们能够唯一确定原树。这里是普通二叉树,不要求满足二叉搜索树的大小关系;判断一个节点属于哪棵子树,要看它在遍历序列中的位置,不能比较数值大小。

解法:递归切分前序与中序区间

核心思路

[!blue]

单看前序可以知道根是谁,却不知道左右子树各有多少节点;中序能够按根的位置划分左右子树,却不会标明哪个元素是根。把两种信息结合起来:用前序首元素确定当前根,再到中序中找到它,根左边就是整棵左子树,右边就是整棵右子树。

定义 build(preRoot, inLeft, inRight):当前子树的根位于前序下标 preRoot,它的全部节点位于中序闭区间 [inLeft, inRight]。每次递归都保持这两个描述指向同一棵子树,因此不必再传前序终点,也不必复制子数组。

设当前根在中序中的下标为 rootIdx,左子树节点数就是 leftSize = rootIdx - inLeft。由于前序中先访问根,再连续访问整棵左子树,最后连续访问右子树,两个递归调用的范围就确定了:

  • 左子树的前序根紧跟当前根,位于 preRoot + 1;中序区间为 [inLeft, rootIdx - 1]。
  • 右子树的前序根要跳过当前根和 leftSize 个左子树节点,位于 preRoot + leftSize + 1;中序区间为 [rootIdx + 1, inRight]。

每次切分都去掉当前根,并把其余节点准确分给左右子树。递归分别还原这两个更小的问题,再接回当前根,就能还原整棵树。当 inLeft > inRight 时区间为空,应先返回空节点,不能再读取前序数组;只有一个节点的区间则会正常建出叶子。

节点值不重复,可以提前建立“节点值到中序下标”的哈希表,让每次找根只需平均 O(1) 时间。这样即使树退化成链,也不需要反复扫描整个剩余中序区间。

解题步骤

  1. 遍历 inorder,建立每个节点值对应的中序下标映射。
  2. 从前序根下标 0 和中序区间 [0, n - 1] 开始递归。
  3. 若中序区间为空,返回空节点;否则以 preorder[preRoot] 创建当前根。
  4. 查出 rootIdx,计算 leftSize = rootIdx - inLeft,按左右子树各自的前序根和中序区间递归。
  5. 将两个递归结果分别接到当前根的左右孩子,返回当前根。

代码实现

class Solution {
    private Map<Integer, Integer> indexMap;

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        indexMap = new HashMap<>();

        for (int i = 0; i < inorder.length; i++) {
            indexMap.put(inorder[i], i);
        }

        return build(preorder, 0, 0, inorder.length - 1);
    }

    // 前序根下标与中序闭区间描述同一子树,空区间要先于前序取值处理。
    private TreeNode build(int[] preorder, int preRoot, int inLeft, int inRight) {
        if (inLeft > inRight) {
            return null;
        }

        int rootVal = preorder[preRoot];
        TreeNode root = new TreeNode(rootVal);
        int rootIdx = indexMap.get(rootVal);
        // 中序区间内根的左侧节点数,决定前序中的右子树起点。
        int leftSize = rootIdx - inLeft;

        root.left = build(preorder, preRoot + 1, inLeft, rootIdx - 1);
        // 跳过当前根和整棵左子树,才到右子树的前序根。
        root.right = build(preorder, preRoot + leftSize + 1, rootIdx + 1, inRight);

        return root;
    }
}
func buildTree(preorder []int, inorder []int) *TreeNode {
    indexMap := make(map[int]int)
    for i, num := range inorder {
        indexMap[num] = i
    }

    var build func(preRoot, inLeft, inRight int) *TreeNode
    // 前序根下标与中序闭区间描述同一子树,空区间要先于前序取值处理。
    build = func(preRoot, inLeft, inRight int) *TreeNode {
        if inLeft > inRight {
            return nil
        }

        rootVal := preorder[preRoot]
        root := &TreeNode{Val: rootVal}
        rootIdx := indexMap[rootVal]
        // 中序区间内根的左侧节点数,决定前序中的右子树起点。
        leftSize := rootIdx - inLeft

        root.Left = build(preRoot+1, inLeft, rootIdx-1)
        // 跳过当前根和整棵左子树,才到右子树的前序根。
        root.Right = build(preRoot+leftSize+1, rootIdx+1, inRight)
        return root
    }

    return build(0, 0, len(inorder)-1)
}

复杂度分析

设节点数为 $n$,树高为 $h$。

  • 时间复杂度:$O(n)$。建立映射和创建节点都只遍历一次,每个节点的根位置查询平均为 $O(1)$。
  • 辅助空间复杂度:$O(n)$。哈希表占 $O(n)$,递归栈占 $O(h)$,树退化成链时 $h=n$;不计返回的树节点占用的空间。

关键点总结

[!green]

  • 前序确定根,中序确定左右子树的归属,节点数量再确定前序中的子树起点。
  • preRoot 与中序闭区间必须始终描述同一棵子树。
  • 节点值互异保证中序位置唯一,传下标和查哈希表可以避免重复扫描、复制数组。

易错点总结

[!yellow]

  • rootIdx 是整份中序数组的下标,左子树大小要减去当前区间起点 inLeft,不能直接使用 rootIdx。
  • 右子树的前序根下标为 preRoot + leftSize + 1;漏加当前根占用的 1 会把左右子树范围错开。
  • 空区间判断是 inLeft > inRight,不是 >=;相等时仍有一个节点。
  • 必须先判断区间为空,再访问 preorder[preRoot],因为空子树对应的前序起点可能已经到达数组末尾。
  • 划分中序时要排除当前根,否则子问题无法持续缩小;也不能把普通二叉树当成二叉搜索树按值大小切分。

相似题目

题目 难度 关联与区别
106. 从中序与后序遍历序列构造二叉树 中等 中序仍负责划分左右子树,原题从后序末尾取根,本题从前序开头取根。
889. 从前序与后序遍历序列构造二叉树 中等 只给前序和后序可能无法唯一确定左右孩子,本题有中序且值互异时分割位置唯一。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/36989078
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!