Files
interview/16-LeetCode Hot 100/从前序与中序遍历序列构造二叉树.md
yasinshaw 5c1c974e88 docs: 改进LeetCode二叉树题目解题思路
按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度:

1. 二叉树的中序遍历
   - 增加"思路推导"部分,解释递归到迭代的转换
   - 详细说明迭代法的每个步骤
   - 增加执行过程演示和多种解法

2. 二叉树的最大深度
   - 增加"思路推导",对比DFS和BFS
   - 详细解释递归的基准情况
   - 增加多种解法和变体问题

3. 从前序与中序遍历序列构造二叉树
   - 详细解释前序和中序的特点
   - 增加"思路推导",说明如何分治
   - 详细说明切片边界计算

4. 对称二叉树
   - 解释镜像对称的定义
   - 详细说明递归比较的逻辑
   - 增加迭代解法和变体问题

5. 翻转二叉树
   - 解释翻转的定义和过程
   - 详细说明多值赋值的执行顺序
   - 增加多种解法和有趣的故事

6. 路径总和
   - 详细解释路径和叶子节点的定义
   - 说明为什么使用递减而非累加
   - 增加多种解法和变体问题

每个文件都包含:
- 完整的示例和边界条件分析
- 详细的算法流程和图解
- 关键细节说明
- 常见错误分析
- 复杂度分析(详细版)
- 执行过程演示
- 多种解法
- 变体问题
- 总结

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 21:33:57 +08:00

15 KiB
Raw Blame History

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

LeetCode 105. 中等

题目描述

给定两个整数数组 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], inorder[i] <= 3000
  • preorderinorder 均无重复元素
  • inorder 保证是二叉树的中序遍历序列
  • preorder 保证是同一棵二叉树的前序遍历序列

思路推导

理解前序和中序遍历

前序遍历顺序: 根 → 左 → 右 中序遍历顺序: 左 → 根 → 右

        3
       / \
      9  20
        /  \
       15   7

前序遍历: [3, 9, 20, 15, 7]
          ↑
         先访问根

中序遍历: [9, 3, 15, 20, 7]
             ↑
          根的左边是左子树
          根的右边是右子树

暴力解法分析

核心观察:

  1. 前序遍历的第一个元素是根节点
  2. 在中序遍历中找到根节点的位置
  3. 根节点左边是左子树,右边是右子树
  4. 递归构造左右子树

图解:

preorder: [3, 9, 20, 15, 7]
           ↑
          根(3)

inorder:  [9, 3, 15, 20, 7]
              ↑
            根(3)
         左子树  右子树
         [9]    [15,20,7]

递归构造:
- 根节点: 3
- 左子树: preorder=[9], inorder=[9]
- 右子树: preorder=[20,15,7], inorder=[15,20,7]

算法步骤:

func buildTree(preorder []int, inorder []int) *TreeNode {
    if len(preorder) == 0 {
        return nil
    }

    // 1. 前序第一个元素是根
    rootVal := preorder[0]
    root := &TreeNode{Val: rootVal}

    // 2. 在中序中找到根的位置
    rootIndex := findIndex(inorder, rootVal)

    // 3. 递归构造左右子树
    root.Left = buildTree(
        preorder[1:1+rootIndex],    // 左子树的前序
        inorder[:rootIndex],         // 左子树的中序
    )
    root.Right = buildTree(
        preorder[1+rootIndex:],      // 右子树的前序
        inorder[rootIndex+1:],       // 右子树的中序
    )

    return root
}

func findIndex(arr []int, target int) int {
    for i, v := range arr {
        if v == target {
            return i
        }
    }
    return -1
}

时间复杂度: O(n²)

  • 每个节点需要查找: O(n)
  • n个节点: O(n²)

空间复杂度: O(n) - 递归栈和切片

问题: 每次查找根节点位置需要O(n),可以优化

优化思考 - 哈希表加速查找

核心问题: 如何快速在中序遍历中找到根节点位置?

优化思路:

  1. 预处理: 用哈希表存储 value → index 的映射
  2. 查找: O(1) 时间找到根节点位置

优化后的算法:

func buildTree(preorder []int, inorder []int) *TreeNode {
    // 建立哈希表: value → index
    indexMap := make(map[int]int)
    for i, v := range inorder {
        indexMap[v] = i
    }

    return build(preorder, 0, len(preorder)-1, 0, indexMap)
}

func build(preorder []int, preStart, preEnd, inStart int, indexMap map[int]int) *TreeNode {
    if preStart > preEnd {
        return nil
    }

    // 前序第一个元素是根
    rootVal := preorder[preStart]
    root := &TreeNode{Val: rootVal}

    // 在中序中找到根的位置O(1)
    rootIndex := indexMap[rootVal]

    // 左子树大小
    leftSize := rootIndex - inStart

    // 递归构造
    root.Left = build(
        preorder,
        preStart+1,          // 左子树前序起点
        preStart+leftSize,   // 左子树前序终点
        inStart,             // 左子树中序起点
        indexMap,
    )
    root.Right = build(
        preorder,
        preStart+leftSize+1, // 右子树前序起点
        preEnd,              // 右子树前序终点
        rootIndex+1,         // 右子树中序起点
        indexMap,
    )

    return root
}

时间复杂度: O(n) - 每个节点只处理一次

为什么这样思考?

核心思想:

  1. 分治: 大问题分解为小问题(构造整棵树 → 构造子树)
  2. 前序确定根: 前序第一个元素就是根
  3. 中序分左右: 根在中序的位置决定左右子树边界
  4. 递归构造: 对左右子树重复上述过程

为什么前序和中序可以唯一确定二叉树?

  • 前序告诉我们"谁是根"
  • 中序告诉我们"哪些是左子树,哪些是右子树"
  • 结合两者可以完全确定树的结构

解题思路

核心思想

利用前序遍历确定根节点,利用中序遍历划分左右子树,递归构造二叉树。

详细算法流程

步骤1: 确定根节点

rootVal := preorder[0]  // 前序遍历第一个元素是根
root := &TreeNode{Val: rootVal}

关键点: 前序遍历的特点是"根左右",第一个元素永远是根

步骤2: 在中序遍历中找到根的位置

rootIndex := findIndex(inorder, rootVal)
// 或用哈希表
rootIndex := indexMap[rootVal]

关键点: 根节点在中序中的位置将数组分为两部分

  • 左边: 左子树的所有节点
  • 右边: 右子树的所有节点

步骤3: 计算左右子树大小

leftSize := rootIndex - inStart  // 左子树节点数
rightSize := len(inorder) - rootIndex - 1  // 右子树节点数

步骤4: 递归构造左右子树

图解:

preorder: [3, 9, 20, 15, 7]
           ↑  ↑--------↑
          根  左子树   右子树

inorder:  [9, 3, 15, 20, 7]
           ↑     ↑       ↑
        左子树  根    右子树

rootIndex = 2  (3在中序中的位置)
leftSize = 2  (左子树有2个节点)

左子树:
- preorder: preorder[1:3] = [9, 20]
- inorder: inorder[0:2] = [9, 3]

右子树:
- preorder: preorder[3:5] = [15, 7]
- inorder: inorder[3:5] = [15, 20, 7]

关键细节说明

细节1: 为什么是 preorder[1:1+rootIndex] 而不是 preorder[1:rootIndex]?

// preorder: [3, 9, 20, 15, 7]
//            ↑  ↑--------↑
//          根(索引0)  左子树(2个元素)

// ❌ 错误写法
root.Left = buildTree(preorder[1:rootIndex], ...)  // [9, 20]? 不对!

// ✅ 正确写法
root.Left = buildTree(preorder[1:1+rootIndex], ...)  // [9]

原因:

  • preorder[0] 是根
  • preorder[1:1+leftSize] 是左子树共leftSize个元素
  • preorder[1+leftSize:] 是右子树

细节2: 为什么 inorder[:rootIndex] 是左子树?

// inorder: [9, 3, 15, 20, 7]
//            ↑     ↑
//          索引0  根在索引2

// 左子树: inorder[0:2] = [9]
// 右子树: inorder[3:5] = [15, 20, 7]

原因: 中序遍历是"左根右",根节点左边全是左子树

细节3: 如何计算左右子树的边界?

// 左子树节点数
leftSize := rootIndex - inStart

// 前序边界
preLeftStart := preStart + 1
preLeftEnd := preStart + leftSize

// 中序边界
inLeftStart := inStart
inLeftEnd := rootIndex - 1

图解:

preorder: [根 | 左子树(leftSize个) | 右子树]
          ↑    ↑               ↑
       preStart preStart+1  preStart+leftSize

inorder:  [左子树(leftSize个) | 根 | 右子树]
          ↑                ↑    ↑
       inStart      rootIndex  rootIndex+1

边界条件分析

边界1: 空树

输入: preorder = [], inorder = []
输出: nil
处理: 直接返回nil

边界2: 只有根节点

输入: preorder = [1], inorder = [1]
输出: TreeNode{Val: 1}
处理:
- rootVal = 1
- rootIndex = 0
- leftSize = 0
- 左子树: preorder[1:1] = [], inorder[0:0] = []
- 右子树: preorder[1:] = [], inorder[1:] = []

边界3: 只有左子树

输入:
preorder: [1, 2]
inorder:  [2, 1]

输出:
  1
 /
2

处理:
- rootVal = 1, rootIndex = 1
- leftSize = 1
- 左子树: preorder[1:2] = [2], inorder[0:1] = [2]
- 右子树: preorder[2:] = [], inorder[2:] = []

边界4: 只有右子树

输入:
preorder: [1, 2]
inorder:  [1, 2]

输出:
1
 \
  2

处理:
- rootVal = 1, rootIndex = 0
- leftSize = 0
- 左子树: preorder[1:1] = [], inorder[0:0] = []
- 右子树: preorder[1:] = [2], inorder[1:] = [2]

复杂度分析(详细版)

方法一: 暴力查找

时间复杂度:

- 构造每个节点: O(1)
- 查找根位置: O(n)
- 总计: n × O(n) = O(n²)

为什么是O(n²)?
- 外层循环: 构造n个节点
- 内层循环: 每次查找O(n)
- 总复杂度: O(n²)

空间复杂度:

- 递归栈: O(h) - h为树高
- 切片: O(n) - 每次创建新切片
- 总计: O(n)

方法二: 哈希表优化

时间复杂度:

- 建立哈希表: O(n)
- 构造每个节点: O(1) - 哈希查找O(1)
- 总计: O(n)

为什么是O(n)?
- 每个节点只处理一次
- 每次处理都是O(1)操作
- 总共n个节点

空间复杂度:

- 哈希表: O(n)
- 递归栈: O(h)
- 总计: O(n)

执行过程演示

输入:

preorder = [3, 9, 20, 15, 7]
inorder = [9, 3, 15, 20, 7]

执行过程:

第1层递归:
├─ preorder[0] = 3 是根
├─ 在inorder中找到3索引=2
├─ 左子树: preorder=[9], inorder=[9]
└─ 右子树: preorder=[20,15,7], inorder=[15,20,7]

第2层递归(左子树):
├─ preorder[0] = 9 是根
├─ 在inorder中找到9索引=0
├─ 左子树: preorder=[], inorder=[]
└─ 右子树: preorder=[], inorder=[]

第2层递归(右子树):
├─ preorder[0] = 20 是根
├─ 在inorder中找到20索引=1
├─ 左子树: preorder=[15], inorder=[15]
└─ 右子树: preorder=[7], inorder=[7]

第3层递归(20的左子树):
├─ preorder[0] = 15 是根
├─ 左子树: []
└─ 右子树: []

第3层递归(20的右子树):
├─ preorder[0] = 7 是根
├─ 左子树: []
└─ 右子树: []

最终构造的树:
      3
     / \
    9  20
      /  \
     15   7

代码实现

方法一: 暴力查找(简单直观)

func buildTree(preorder []int, inorder []int) *TreeNode {
    if len(preorder) == 0 {
        return nil
    }

    root := &TreeNode{Val: preorder[0]}
    index := findIndex(inorder, preorder[0])

    root.Left = buildTree(
        preorder[1:1+index],
        inorder[:index],
    )
    root.Right = buildTree(
        preorder[1+index:],
        inorder[index+1:],
    )

    return root
}

func findIndex(arr []int, target int) int {
    for i, v := range arr {
        if v == target {
            return i
        }
    }
    return -1
}

复杂度: O(n²) 时间O(n) 空间

方法二: 哈希表优化(推荐)

func buildTree(preorder []int, inorder []int) *TreeNode {
    // 建立哈希表: value → index
    indexMap := make(map[int]int)
    for i, v := range inorder {
        indexMap[v] = i
    }

    return build(
        preorder,
        0,
        len(preorder)-1,
        0,
        indexMap,
    )
}

func build(
    preorder []int,
    preStart, preEnd int,
    inStart int,
    indexMap map[int]int,
) *TreeNode {
    if preStart > preEnd {
        return nil
    }

    // 前序第一个元素是根
    rootVal := preorder[preStart]
    root := &TreeNode{Val: rootVal}

    // 在中序中找到根的位置
    rootIndex := indexMap[rootVal]

    // 左子树大小
    leftSize := rootIndex - inStart

    // 递归构造左右子树
    root.Left = build(
        preorder,
        preStart+1,          // 左子树前序起点
        preStart+leftSize,   // 左子树前序终点
        inStart,             // 左子树中序起点
        indexMap,
    )
    root.Right = build(
        preorder,
        preStart+leftSize+1, // 右子树前序起点
        preEnd,              // 右子树前序终点
        rootIndex+1,         // 右子树中序起点
        indexMap,
    )

    return root
}

复杂度: O(n) 时间O(n) 空间

常见错误

错误1: 切片边界错误

错误写法:

root.Left = buildTree(preorder[1:index], inorder[:index])

正确写法:

root.Left = buildTree(preorder[1:1+index], inorder[:index])

原因: 左子树有index个元素应该从1到1+index

错误2: 忘记处理空数组

错误写法:

func buildTree(preorder []int, inorder []int) *TreeNode {
    rootVal := preorder[0]  // 可能越界!
    // ...
}

正确写法:

func buildTree(preorder []int, inorder []int) *TreeNode {
    if len(preorder) == 0 {
        return nil
    }
    rootVal := preorder[0]
    // ...
}

原因: 空数组没有第0个元素

错误3: 左右子树边界混淆

错误写法:

// 错误地认为右子树从rootIndex开始
root.Right = buildTree(preorder[1+index:], inorder[rootIndex:])

正确写法:

// 右子树从rootIndex+1开始跳过根节点
root.Right = buildTree(preorder[1+index:], inorder[rootIndex+1:])

原因: inorder[rootIndex] 是根节点,右子树应该从 rootIndex+1 开始

变体问题

变体1: 从中序与后序遍历构造二叉树

后序遍历: 左 → 右 → 根(最后一个元素是根)

func buildTree(inorder []int, postorder []int) *TreeNode {
    indexMap := make(map[int]int)
    for i, v := range inorder {
        indexMap[v] = i
    }

    return build(
        postorder,
        len(postorder)-1,  // 从最后一个元素开始
        0,
        len(inorder)-1,
        indexMap,
    )
}

func build(
    postorder []int,
    postStart, inStart, inEnd int,
    indexMap map[int]int,
) *TreeNode {
    if inStart > inEnd {
        return nil
    }

    rootVal := postorder[postStart]
    root := &TreeNode{Val: rootVal}

    rootIndex := indexMap[rootVal]
    rightSize := inEnd - rootIndex

    root.Right = build(
        postorder,
        postStart-1,
        rootIndex+1,
        inEnd,
        indexMap,
    )
    root.Left = build(
        postorder,
        postStart-1-rightSize,
        inStart,
        rootIndex-1,
        indexMap,
    )

    return root
}

变体2: 从前序与后序遍历构造二叉树

注意: 前序+后序无法唯一确定二叉树(除非是满二叉树)

变体3: 判断给定数组是否是某棵树的前序/中序遍历

需要验证数组是否满足遍历的性质(略)

总结

核心要点:

  1. 前序定根: preorder[0] 永远是当前子树的根
  2. 中序分界: 根在中序的位置决定左右子树边界
  3. 递归构造: 对左右子树重复相同过程
  4. 哈希优化: 用哈希表将查找优化到O(1)

易错点:

  • 切片边界计算错误(1:1+index vs 1:index
  • 忘记处理空数组
  • 左右子树边界混淆(inorder[rootIndex:] vs inorder[rootIndex+1:]

关键规律:

前序: [根 | 左子树 | 右子树]
中序: [左子树 | 根 | 右子树]

通过前序找根,通过中序分左右

推荐写法: 哈希表优化版O(n)时间)