按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度: 1. 二叉树的中序遍历 - 增加"思路推导"部分,解释递归到迭代的转换 - 详细说明迭代法的每个步骤 - 增加执行过程演示和多种解法 2. 二叉树的最大深度 - 增加"思路推导",对比DFS和BFS - 详细解释递归的基准情况 - 增加多种解法和变体问题 3. 从前序与中序遍历序列构造二叉树 - 详细解释前序和中序的特点 - 增加"思路推导",说明如何分治 - 详细说明切片边界计算 4. 对称二叉树 - 解释镜像对称的定义 - 详细说明递归比较的逻辑 - 增加迭代解法和变体问题 5. 翻转二叉树 - 解释翻转的定义和过程 - 详细说明多值赋值的执行顺序 - 增加多种解法和有趣的故事 6. 路径总和 - 详细解释路径和叶子节点的定义 - 说明为什么使用递减而非累加 - 增加多种解法和变体问题 每个文件都包含: - 完整的示例和边界条件分析 - 详细的算法流程和图解 - 关键细节说明 - 常见错误分析 - 复杂度分析(详细版) - 执行过程演示 - 多种解法 - 变体问题 - 总结 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
697 lines
15 KiB
Markdown
697 lines
15 KiB
Markdown
# 从前序与中序遍历序列构造二叉树
|
||
|
||
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)时间)
|