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

697 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 从前序与中序遍历序列构造二叉树
LeetCode 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` 保证是同一棵二叉树的前序遍历序列
## 思路推导
### 理解前序和中序遍历
**前序遍历顺序**: 根 → 左 → 右
**中序遍历顺序**: 左 → 根 → 右
```
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]
```
**算法步骤**:
```go
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) 时间找到根节点位置
**优化后的算法**:
```go
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: 确定根节点**
```go
rootVal := preorder[0] // 前序遍历第一个元素是根
root := &TreeNode{Val: rootVal}
```
**关键点**: 前序遍历的特点是"根左右",第一个元素永远是根
**步骤2: 在中序遍历中找到根的位置**
```go
rootIndex := findIndex(inorder, rootVal)
// 或用哈希表
rootIndex := indexMap[rootVal]
```
**关键点**: 根节点在中序中的位置将数组分为两部分
- 左边: 左子树的所有节点
- 右边: 右子树的所有节点
**步骤3: 计算左右子树大小**
```go
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]`?**
```go
// 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]` 是左子树?**
```go
// inorder: [9, 3, 15, 20, 7]
// ↑ ↑
// 索引0 根在索引2
// 左子树: inorder[0:2] = [9]
// 右子树: inorder[3:5] = [15, 20, 7]
```
**原因**: 中序遍历是"左根右",根节点左边全是左子树
**细节3: 如何计算左右子树的边界?**
```go
// 左子树节点数
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
```
## 代码实现
### 方法一: 暴力查找(简单直观)
```go
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) 空间
### 方法二: 哈希表优化(推荐)
```go
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: 切片边界错误
**错误写法**:
```go
root.Left = buildTree(preorder[1:index], inorder[:index])
```
**正确写法**:
```go
root.Left = buildTree(preorder[1:1+index], inorder[:index])
```
**原因**: 左子树有index个元素应该从1到1+index
### 错误2: 忘记处理空数组
**错误写法**:
```go
func buildTree(preorder []int, inorder []int) *TreeNode {
rootVal := preorder[0] // 可能越界!
// ...
}
```
**正确写法**:
```go
func buildTree(preorder []int, inorder []int) *TreeNode {
if len(preorder) == 0 {
return nil
}
rootVal := preorder[0]
// ...
}
```
**原因**: 空数组没有第0个元素
### 错误3: 左右子树边界混淆
**错误写法**:
```go
// 错误地认为右子树从rootIndex开始
root.Right = buildTree(preorder[1+index:], inorder[rootIndex:])
```
**正确写法**:
```go
// 右子树从rootIndex+1开始跳过根节点
root.Right = buildTree(preorder[1+index:], inorder[rootIndex+1:])
```
**原因**: `inorder[rootIndex]` 是根节点,右子树应该从 `rootIndex+1` 开始
## 变体问题
### 变体1: 从中序与后序遍历构造二叉树
**后序遍历**: 左 → 右 → 根(最后一个元素是根)
```go
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)时间)