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

669 lines
12 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.
# 二叉树的中序遍历 (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`
**推荐写法**: 迭代法(空间可控,面试常考)