105. 从前序与中序遍历序列构造二叉树 (Medium)

专题归类: 05-二叉树 LeetCode 链接: https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/


在线做题: 写 Python 代码并运行 · Java 跳转 LeetCode

题目描述

给定两个整数数组 preorderinorder,其中 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] <= 3000
  • preorderinorder无重复 元素
  • inorder 均出现在 preorder
  • preorder 保证为二叉树的前序遍历序列
  • inorder 保证为二叉树的中序遍历序列

题目详细分析

  • 数据范围含义: 最多 3000 个节点,值覆盖 [-3000, 3000],无重复值。无重复值意味着可以用哈希表将中序值映射到索引。
  • 输入输出特征: 输入前序和中序两个数组,输出树的根节点。两个数组长度相等,且都是有效的遍历序列。
  • 边界条件: 单节点时直接返回该节点;空区间返回 None。
  • 核心约束: 必须用两种遍历结果唯一确定一棵二叉树。前序确定根节点,中序确定左右子树范围。
  • 隐藏条件: 二叉树中无重复值,这是能用哈希表映射的前提。如果有重复值,就无法用值唯一确定位置,题目会更复杂。

小白版直白理解

就像你拿到了一个家族的两份家谱记录——第一份是按「先长辈后晚辈」的顺序记录所有人(前序:根→左→右),第二份是按「左半边家族→长辈→右半边家族」的顺序记录(中序:左→根→右)。你需要还原出完整的家族树。方法:从第一份记录中找出族长(第一个就是根),然后在第二份记录中找到族长在列表中的位置,左边就是左半边家族,右边就是右半边家族。对左右半边重复这个步骤。


解题思路

思路一:递归 + 哈希表(推荐)

核心思想:

  1. 前序的第一个元素是根节点。
  2. 在中序中找到根节点的位置,左边是左子树的中序,右边是右子树的中序。
  3. 根据左子树的节点数,可以从前序中划分出左子树的前序和右子树的前序。
  4. 递归构建左右子树。

关键: 用哈希表(字典)存储中序数组中每个值的索引,这样查找根节点在中序中的位置就是 O(1)。

def buildTree(preorder, inorder):
    # 哈希表:值 → 在中序数组中的索引
    index_map = {val: i for i, val in enumerate(inorder)}
 
    def build(pre_l, pre_r, in_l, in_r):
        """构建子树
        Args:
            pre_l, pre_r: 前序数组的左右边界(闭区间)
            in_l, in_r: 中序数组的左右边界(闭区间)
        """
        if pre_l > pre_r:
            return None
 
        # 前序的第一个元素是根节点
        root_val = preorder[pre_l]
        root = TreeNode(root_val)
 
        # 在中序中找到根节点的位置
        in_idx = index_map[root_val]
        # 左子树的节点数
        left_size = in_idx - in_l
 
        # 递归构建左右子树
        # 前序中:根后 left_size 个是左子树前序,剩下的是右子树前序
        root.left = build(pre_l + 1, pre_l + left_size, in_l, in_idx - 1)
        root.right = build(pre_l + left_size + 1, pre_r, in_idx + 1, in_r)
 
        return root
 
    return build(0, len(preorder) - 1, 0, len(inorder) - 1)

思路二:递归 + 指针扫描(不建哈希表)

用一个指针 pre_idx 扫描前序数组(全局变量),在中序数组中定位当前根节点,利用中序的左右范围分割左右子树。

def buildTree(preorder, inorder):
    pre_idx = 0  # 前序数组的指针
 
    def build(in_l, in_r):
        nonlocal pre_idx
        if in_l > in_r:
            return None
 
        # 取前序当前指针的值作为根节点
        root_val = preorder[pre_idx]
        root = TreeNode(root_val)
        pre_idx += 1
 
        # 在中序中找到根节点位置
        in_idx = inorder.index(root_val)  # O(n) 查找,可以优化为哈希表
 
        # 注意:必须先构建左子树!
        # 因为前序的顺序是「根→左→右」,指针前移意味着先找到左子树的根
        root.left = build(in_l, in_idx - 1)
        root.right = build(in_idx + 1, in_r)
 
        return root
 
    return build(0, len(inorder) - 1)

注意:这种方式如果不配合哈希表,每次 inorder.index() 是 O(n),总时间复杂度为 O(n


易错点

  • 左子树大小的计算: left_size = in_idx - in_l(中序中根的位置减左边界),然后用 left_size 来分割前序数组。
  • 前序数组边界: 左子树前序区间是 [pre_l + 1, pre_l + left_size],右子树前序区间是 [pre_l + left_size + 1, pre_r]。注意 +1 和 -1 的边界细节。
  • 递归的终止条件: 当区间左边界 > 右边界时返回 None,表示没有子节点。不是 >=,因为 == 时还有一个节点需要构建。
  • 无重复值是前提: 如果有重复值,哈希表映射会有歧义。题目保证无重复。
  • 先构建左子树再构建右子树: 前序的顺序是「根→左→右」,所以递归构建时也必须先左后右,这样才能和 pre_idx 指针的移动保持一致。

框架提炼

从前序+中序构建二叉树的标准模板:

def buildTree(preorder, inorder):
    index_map = {val: i for i, val in enumerate(inorder)}
 
    def build(pre_l, pre_r, in_l, in_r):
        if pre_l > pre_r:
            return None
        root = TreeNode(preorder[pre_l])
        in_idx = index_map[root.val]
        left_size = in_idx - in_l
        root.left = build(pre_l + 1, pre_l + left_size, in_l, in_idx - 1)
        root.right = build(pre_l + left_size + 1, pre_r, in_idx + 1, in_r)
        return root
 
    return build(0, len(preorder) - 1, 0, len(inorder) - 1)

推广到其他遍历组合:

  • 中序 + 后序: 后序的最后一个元素是根,同样在中序中找到根的位置分割。
  • 前序 + 后序: 不能唯一确定二叉树(除非是满二叉树/真二叉树)。

关键公式: left_size = in_idx - in_l 这一步是核心,连接了前序和中序两个数组。


关联题目