按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度: 1. 二叉树的中序遍历 - 增加"思路推导"部分,解释递归到迭代的转换 - 详细说明迭代法的每个步骤 - 增加执行过程演示和多种解法 2. 二叉树的最大深度 - 增加"思路推导",对比DFS和BFS - 详细解释递归的基准情况 - 增加多种解法和变体问题 3. 从前序与中序遍历序列构造二叉树 - 详细解释前序和中序的特点 - 增加"思路推导",说明如何分治 - 详细说明切片边界计算 4. 对称二叉树 - 解释镜像对称的定义 - 详细说明递归比较的逻辑 - 增加迭代解法和变体问题 5. 翻转二叉树 - 解释翻转的定义和过程 - 详细说明多值赋值的执行顺序 - 增加多种解法和有趣的故事 6. 路径总和 - 详细解释路径和叶子节点的定义 - 说明为什么使用递减而非累加 - 增加多种解法和变体问题 每个文件都包含: - 完整的示例和边界条件分析 - 详细的算法流程和图解 - 关键细节说明 - 常见错误分析 - 复杂度分析(详细版) - 执行过程演示 - 多种解法 - 变体问题 - 总结 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
669 lines
12 KiB
Markdown
669 lines
12 KiB
Markdown
# 二叉树的中序遍历 (Binary Tree Inorder Traversal)
|
||
|
||
LeetCode 94. 简单
|
||
|
||
## 题目描述
|
||
|
||
给定一个二叉树的根节点,返回它的中序遍历。
|
||
|
||
**示例 1:**
|
||
```
|
||
输入:root = [1,null,2,3]
|
||
输出:[1,3,2]
|
||
```
|
||
|
||
**示例 2:**
|
||
```
|
||
输入:root = []
|
||
输出:[]
|
||
```
|
||
|
||
**示例 3:**
|
||
```
|
||
输入:root = [1]
|
||
输出:[1]
|
||
```
|
||
|
||
## 思路推导
|
||
|
||
### 什么是中序遍历?
|
||
|
||
中序遍历的顺序是:**左子树 → 根节点 → 右子树**
|
||
|
||
```
|
||
1
|
||
/ \
|
||
2 3
|
||
/ \
|
||
4 5
|
||
|
||
中序遍历: [4, 2, 5, 1, 3]
|
||
↑ ↑ ↑
|
||
左 根 右
|
||
```
|
||
|
||
### 暴力解法分析
|
||
|
||
**递归解法 - 最直观的思路**
|
||
|
||
```go
|
||
func inorderTraversal(root *TreeNode) []int {
|
||
result := []int{}
|
||
inorder(root, &result)
|
||
return result
|
||
}
|
||
|
||
func inorder(node *TreeNode, result *[]int) {
|
||
if node == nil {
|
||
return
|
||
}
|
||
// 1. 先遍历左子树
|
||
inorder(node.Left, result)
|
||
// 2. 再访问根节点
|
||
*result = append(*result, node.Val)
|
||
// 3. 最后遍历右子树
|
||
inorder(node.Right, result)
|
||
}
|
||
```
|
||
|
||
**时间复杂度**: O(n) - 每个节点访问一次
|
||
**空间复杂度**: O(h) - h为树高,递归栈空间
|
||
|
||
**问题**: 递归解法虽然简单,但面试官常要求用迭代实现
|
||
|
||
### 优化思考 - 递归转迭代
|
||
|
||
**核心问题**: 如何用栈模拟递归的调用过程?
|
||
|
||
**观察递归的执行过程**:
|
||
```
|
||
inorder(1):
|
||
inorder(2): <- 栈帧1
|
||
inorder(4): <- 栈帧2
|
||
访问4 <- 栈帧3
|
||
访问2
|
||
inorder(5):
|
||
访问5
|
||
访问1
|
||
inorder(3):
|
||
访问3
|
||
```
|
||
|
||
**关键发现**:
|
||
1. 递归调用就是**压栈**
|
||
2. 递归返回就是**出栈**
|
||
3. 中序遍历先找**最左节点**,再逐层返回
|
||
|
||
### 为什么迭代法这样写?
|
||
|
||
**核心思想**:
|
||
1. 一直往左走,把路径上的节点都入栈
|
||
2. 到达最左节点后,出栈并访问
|
||
3. 转向右子树,重复步骤1
|
||
|
||
**为什么这样思考?**
|
||
- 模拟递归的调用栈
|
||
- 利用栈的"后进先出"特性
|
||
- 先保存路径,再反向访问
|
||
|
||
## 解题思路
|
||
|
||
### 方法一:递归(直观但空间开销大)
|
||
|
||
### 核心思想
|
||
按照"左→根→右"的顺序递归访问节点。
|
||
|
||
### 算法流程
|
||
|
||
**步骤1: 定义递归函数**
|
||
```go
|
||
func inorder(node *TreeNode, result *[]int)
|
||
```
|
||
|
||
**步骤2: 递归终止条件**
|
||
```go
|
||
if node == nil {
|
||
return // 空节点直接返回
|
||
}
|
||
```
|
||
|
||
**步骤3: 按顺序递归**
|
||
```go
|
||
// 1. 先遍历左子树
|
||
inorder(node.Left, result)
|
||
|
||
// 2. 再访问根节点
|
||
*result = append(*result, node.Val)
|
||
|
||
// 3. 最后遍历右子树
|
||
inorder(node.Right, result)
|
||
```
|
||
|
||
### 方法二:迭代(栈模拟递归)
|
||
|
||
### 核心思想
|
||
用显式栈替代递归调用栈,模拟递归的执行过程。
|
||
|
||
### 详细算法流程
|
||
|
||
**步骤1: 初始化数据结构**
|
||
```go
|
||
result := []int{} // 存储遍历结果
|
||
stack := []*TreeNode{} // 模拟递归栈
|
||
curr := root // 当前访问的节点
|
||
```
|
||
|
||
**步骤2: 外层循环 - 只要还有节点可访问**
|
||
```go
|
||
for curr != nil || len(stack) > 0 {
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**关键点**:
|
||
- `curr != nil`: 还有节点需要处理
|
||
- `len(stack) > 0`: 栈中还有未访问的节点
|
||
- 两个条件满足一个就可以继续
|
||
|
||
**步骤3: 内层循环 - 一直往左走,找到最左节点**
|
||
```go
|
||
for curr != nil {
|
||
stack = append(stack, curr) // 路径上的节点都入栈
|
||
curr = curr.Left // 继续往左走
|
||
}
|
||
```
|
||
|
||
**图解**:
|
||
```
|
||
1
|
||
/ \
|
||
2 3
|
||
/ \
|
||
4 5
|
||
|
||
第一次内层循环后:
|
||
stack: [1, 2, 4] <- 从根到最左的路径
|
||
curr: nil <- 4的左子树为空
|
||
```
|
||
|
||
**步骤4: 出栈并访问**
|
||
```go
|
||
curr = stack[len(stack)-1] // 取栈顶元素
|
||
stack = stack[:len(stack)-1] // 出栈
|
||
result = append(result, curr.Val) // 访问该节点
|
||
```
|
||
|
||
**图解**:
|
||
```
|
||
出栈4并访问:
|
||
stack: [1, 2]
|
||
result: [4]
|
||
curr: 4
|
||
```
|
||
|
||
**步骤5: 转向右子树**
|
||
```go
|
||
curr = curr.Right
|
||
```
|
||
|
||
**图解**:
|
||
```
|
||
4的右子树为空,进入下次循环:
|
||
stack: [1, 2]
|
||
curr: nil
|
||
|
||
再次出栈2并访问:
|
||
stack: [1]
|
||
result: [4, 2]
|
||
curr: 2
|
||
|
||
转向2的右子树:
|
||
curr: 5
|
||
```
|
||
|
||
### 关键细节说明
|
||
|
||
**细节1: 为什么是 `for curr != nil || len(stack) > 0`?**
|
||
|
||
```go
|
||
// ❌ 错误写法
|
||
for curr != nil { // 会漏掉栈中剩余的节点
|
||
|
||
// ✅ 正确写法
|
||
for curr != nil || len(stack) > 0 {
|
||
```
|
||
|
||
**原因**:
|
||
- `curr == nil` 时,栈中可能还有节点
|
||
- `len(stack) == 0` 时,curr可能指向某个右子树
|
||
- 两个条件需要同时考虑
|
||
|
||
**细节2: 为什么是 `curr = curr.Right` 而不是继续往左?**
|
||
|
||
```go
|
||
// 出栈并访问后
|
||
result = append(result, curr.Val)
|
||
curr = curr.Right // 转向右子树
|
||
```
|
||
|
||
**原因**: 中序遍历顺序是"左→根→右"
|
||
- 访问完根节点后,该访问右子树了
|
||
- 右子树也要按"左→根→右"遍历
|
||
- 下次循环会从右子树的最左节点开始
|
||
|
||
**细节3: 为什么内层循环要一直往左走?**
|
||
|
||
```go
|
||
for curr != nil {
|
||
stack = append(stack, curr)
|
||
curr = curr.Left // 一直往左
|
||
}
|
||
```
|
||
|
||
**原因**: 中序遍历先访问左子树
|
||
- 需要找到最左节点(第一个要访问的节点)
|
||
- 保存路径上的所有节点(之后要逐个访问)
|
||
- 利用栈的LIFO特性,反向访问
|
||
|
||
### 边界条件分析
|
||
|
||
**边界1: 空树**
|
||
```
|
||
输入: root = nil
|
||
输出: []
|
||
处理: 直接返回空数组
|
||
```
|
||
|
||
**边界2: 只有根节点**
|
||
```
|
||
输入: root = [1]
|
||
输出: [1]
|
||
过程:
|
||
1. curr=1入栈
|
||
2. curr=1.Left=nil
|
||
3. 出栈1,访问
|
||
4. curr=1.Right=nil
|
||
5. 栈空,curr=nil,退出
|
||
```
|
||
|
||
**边界3: 只有左子树**
|
||
```
|
||
输入:
|
||
1
|
||
/
|
||
2
|
||
/
|
||
3
|
||
|
||
输出: [3, 2, 1]
|
||
过程: 一直往左,找到3,再逐层返回
|
||
```
|
||
|
||
**边界4: 只有右子树**
|
||
```
|
||
输入:
|
||
1
|
||
\
|
||
2
|
||
\
|
||
3
|
||
|
||
输出: [1, 2, 3]
|
||
过程:
|
||
1. 1入栈,curr=nil
|
||
2. 出栈1,访问,curr=2
|
||
3. 2入栈,curr=nil
|
||
4. 出栈2,访问,curr=3
|
||
5. 3入栈,curr=nil
|
||
6. 出栈3,访问,curr=nil
|
||
7. 栈空,退出
|
||
```
|
||
|
||
### 复杂度分析(详细版)
|
||
|
||
**时间复杂度**:
|
||
```
|
||
- 外层循环: O(n) - 每个节点入栈出栈一次
|
||
- 内层循环: 总计O(n) - 每个节点被访问一次
|
||
- 总计: O(n)
|
||
|
||
为什么每个节点只访问一次?
|
||
- 入栈: 一次
|
||
- 出栈: 一次
|
||
- 访问: 一次
|
||
- 没有重复操作
|
||
```
|
||
|
||
**空间复杂度**:
|
||
```
|
||
- 栈空间: O(h) - h为树高
|
||
- 最坏情况(链状树): O(n)
|
||
- 最好情况(完全平衡树): O(log n)
|
||
- 结果存储: O(n)
|
||
- 总计: O(n)
|
||
```
|
||
|
||
### 执行过程演示
|
||
|
||
**输入**:
|
||
```
|
||
1
|
||
/ \
|
||
2 3
|
||
/ \
|
||
4 5
|
||
```
|
||
|
||
**执行过程**:
|
||
```
|
||
初始状态:
|
||
stack: []
|
||
result: []
|
||
curr: 1
|
||
|
||
第1次内层循环(往左走):
|
||
stack: [1, 2, 4]
|
||
curr: nil
|
||
|
||
出栈4:
|
||
stack: [1, 2]
|
||
result: [4]
|
||
curr: nil (4.Right)
|
||
|
||
出栈2:
|
||
stack: [1]
|
||
result: [4, 2]
|
||
curr: 5
|
||
|
||
第2次内层循环(从5开始):
|
||
stack: [1, 5]
|
||
curr: nil
|
||
|
||
出栈5:
|
||
stack: [1]
|
||
result: [4, 2, 5]
|
||
curr: nil (5.Right)
|
||
|
||
出栈1:
|
||
stack: []
|
||
result: [4, 2, 5, 1]
|
||
curr: 3
|
||
|
||
第3次内层循环(从3开始):
|
||
stack: [3]
|
||
curr: nil
|
||
|
||
出栈3:
|
||
stack: []
|
||
result: [4, 2, 5, 1, 3]
|
||
curr: nil (3.Right)
|
||
|
||
退出循环:
|
||
final result: [4, 2, 5, 1, 3]
|
||
```
|
||
|
||
## 代码实现
|
||
|
||
### 方法一:递归
|
||
|
||
```go
|
||
func inorderTraversal(root *TreeNode) []int {
|
||
result := []int{}
|
||
inorder(root, &result)
|
||
return result
|
||
}
|
||
|
||
func inorder(node *TreeNode, result *[]int) {
|
||
if node == nil {
|
||
return
|
||
}
|
||
// 1. 先遍历左子树
|
||
inorder(node.Left, result)
|
||
// 2. 再访问根节点
|
||
*result = append(*result, node.Val)
|
||
// 3. 最后遍历右子树
|
||
inorder(node.Right, result)
|
||
}
|
||
```
|
||
|
||
**复杂度**: O(n) 时间,O(h) 空间
|
||
|
||
### 方法二:迭代(推荐)
|
||
|
||
```go
|
||
func inorderTraversal(root *TreeNode) []int {
|
||
result := []int{}
|
||
stack := []*TreeNode{}
|
||
curr := root
|
||
|
||
// 只要还有节点可访问
|
||
for curr != nil || len(stack) > 0 {
|
||
// 一直往左走,把路径上的节点都入栈
|
||
for curr != nil {
|
||
stack = append(stack, curr)
|
||
curr = curr.Left
|
||
}
|
||
|
||
// 出栈并访问
|
||
curr = stack[len(stack)-1]
|
||
stack = stack[:len(stack)-1]
|
||
result = append(result, curr.Val)
|
||
|
||
// 转向右子树
|
||
curr = curr.Right
|
||
}
|
||
|
||
return result
|
||
}
|
||
```
|
||
|
||
**复杂度**: O(n) 时间,O(n) 空间
|
||
|
||
### 方法三:Morris遍历(空间O(1))
|
||
|
||
```go
|
||
func inorderTraversal(root *TreeNode) []int {
|
||
result := []int{}
|
||
curr := root
|
||
|
||
for curr != nil {
|
||
if curr.Left == nil {
|
||
// 没有左子树,访问当前节点
|
||
result = append(result, curr.Val)
|
||
curr = curr.Right
|
||
} else {
|
||
// 找到左子树的最右节点(前驱节点)
|
||
prev := curr.Left
|
||
for prev.Right != nil && prev.Right != curr {
|
||
prev = prev.Right
|
||
}
|
||
|
||
if prev.Right == nil {
|
||
// 第一次访问,建立线索
|
||
prev.Right = curr
|
||
curr = curr.Left
|
||
} else {
|
||
// 第二次访问,删除线索并访问当前节点
|
||
prev.Right = nil
|
||
result = append(result, curr.Val)
|
||
curr = curr.Right
|
||
}
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
```
|
||
|
||
**复杂度**: O(n) 时间,O(1) 空间
|
||
|
||
**原理**: 利用空指针存储临时信息,避免使用栈
|
||
|
||
## 常见错误
|
||
|
||
### 错误1: 循环条件错误
|
||
|
||
❌ **错误写法**:
|
||
```go
|
||
for curr != nil { // 漏掉了栈中还有节点的情况
|
||
// ...
|
||
}
|
||
```
|
||
|
||
✅ **正确写法**:
|
||
```go
|
||
for curr != nil || len(stack) > 0 {
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**原因**: 当 `curr == nil` 时,栈中可能还有未访问的节点
|
||
|
||
### 错误2: 出栈后忘记转向右子树
|
||
|
||
❌ **错误写法**:
|
||
```go
|
||
curr = stack[len(stack)-1]
|
||
stack = stack[:len(stack)-1]
|
||
result = append(result, curr.Val)
|
||
// 缺少这一行: curr = curr.Right
|
||
```
|
||
|
||
✅ **正确写法**:
|
||
```go
|
||
curr = stack[len(stack)-1]
|
||
stack = stack[:len(stack)-1]
|
||
result = append(result, curr.Val)
|
||
curr = curr.Right // 重要:转向右子树
|
||
```
|
||
|
||
**原因**: 访问完根节点后,必须访问右子树
|
||
|
||
### 错误3: 内层循环条件错误
|
||
|
||
❌ **错误写法**:
|
||
```go
|
||
for curr.Left != nil { // 错误:会导致最左节点不入栈
|
||
stack = append(stack, curr)
|
||
curr = curr.Left
|
||
}
|
||
```
|
||
|
||
✅ **正确写法**:
|
||
```go
|
||
for curr != nil {
|
||
stack = append(stack, curr)
|
||
curr = curr.Left
|
||
}
|
||
```
|
||
|
||
**原因**: 最左节点也需要入栈,然后出栈访问
|
||
|
||
## 变体问题
|
||
|
||
### 变体1: 前序遍历
|
||
|
||
**顺序**: 根 → 左 → 右
|
||
|
||
```go
|
||
func preorderTraversal(root *TreeNode) []int {
|
||
result := []int{}
|
||
stack := []*TreeNode{}
|
||
curr := root
|
||
|
||
for curr != nil || len(stack) > 0 {
|
||
for curr != nil {
|
||
result = append(result, curr.Val) // 先访问
|
||
stack = append(stack, curr)
|
||
curr = curr.Left
|
||
}
|
||
curr = stack[len(stack)-1]
|
||
stack = stack[:len(stack)-1]
|
||
curr = curr.Right
|
||
}
|
||
|
||
return result
|
||
}
|
||
```
|
||
|
||
### 变体2: 后序遍历
|
||
|
||
**顺序**: 左 → 右 → 根
|
||
|
||
```go
|
||
func postorderTraversal(root *TreeNode) []int {
|
||
result := []int{}
|
||
stack := []*TreeNode{}
|
||
curr := root
|
||
var lastVisited *TreeNode
|
||
|
||
for curr != nil || len(stack) > 0 {
|
||
for curr != nil {
|
||
stack = append(stack, curr)
|
||
curr = curr.Left
|
||
}
|
||
|
||
curr = stack[len(stack)-1]
|
||
if curr.Right == nil || curr.Right == lastVisited {
|
||
stack = stack[:len(stack)-1]
|
||
result = append(result, curr.Val)
|
||
lastVisited = curr
|
||
curr = nil
|
||
} else {
|
||
curr = curr.Right
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
```
|
||
|
||
### 变体3: 层序遍历(BFS)
|
||
|
||
使用队列而非栈:
|
||
|
||
```go
|
||
func levelOrder(root *TreeNode) [][]int {
|
||
if root == nil {
|
||
return [][]int{}
|
||
}
|
||
|
||
result := [][]int{}
|
||
queue := []*TreeNode{root}
|
||
|
||
for len(queue) > 0 {
|
||
level := []int{}
|
||
size := len(queue)
|
||
for i := 0; i < size; i++ {
|
||
node := queue[0]
|
||
queue = queue[1:]
|
||
level = append(level, node.Val)
|
||
if node.Left != nil {
|
||
queue = append(queue, node.Left)
|
||
}
|
||
if node.Right != nil {
|
||
queue = append(queue, node.Right)
|
||
}
|
||
}
|
||
result = append(result, level)
|
||
}
|
||
|
||
return result
|
||
}
|
||
```
|
||
|
||
## 总结
|
||
|
||
**核心要点**:
|
||
1. **中序遍历顺序**: 左 → 根 → 右
|
||
2. **迭代法核心**: 用栈模拟递归调用
|
||
3. **关键操作**: 一直往左 → 出栈访问 → 转向右边
|
||
4. **循环条件**: `curr != nil || len(stack) > 0`
|
||
|
||
**易错点**:
|
||
- 循环条件容易漏掉栈的情况
|
||
- 出栈后忘记转向右子树
|
||
- 内层循环条件错误(应该用 `curr != nil`)
|
||
|
||
**推荐写法**: 迭代法(空间可控,面试常考)
|