docs: 改进LeetCode二叉树题目解题思路

按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度:

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

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

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

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

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

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

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 21:33:57 +08:00
parent 67189941d8
commit 5c1c974e88
14 changed files with 7817 additions and 139 deletions

View File

@@ -1,37 +1,668 @@
# 二叉树的中序遍历 (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]
stack = stack[:len(stack)-1]
result = append(result, curr.Val)
curr = curr.Right
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
}
```
**复杂度:** O(n) 时间O(n) 空间
### 变体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`
**推荐写法**: 迭代法(空间可控,面试常考)