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:
@@ -1,24 +1,604 @@
|
||||
# 翻转二叉树 (Invert Binary Tree)
|
||||
|
||||
LeetCode 226. 简单
|
||||
|
||||
## 题目描述
|
||||
|
||||
给你一棵二叉树的根节点 root,翻转这棵二叉树,并返回其根节点。
|
||||
给你一棵二叉树的根节点 `root`,翻转这棵二叉树,并返回其根节点。
|
||||
|
||||
**示例 1:**
|
||||
```
|
||||
输入:root = [4,2,7,1,3,6,9]
|
||||
输出:[4,7,2,9,6,3,1]
|
||||
```
|
||||
|
||||
**示例 2:**
|
||||
```
|
||||
输入:root = [2,1,3]
|
||||
输出:[2,3,1]
|
||||
```
|
||||
|
||||
**示例 3:**
|
||||
```
|
||||
输入:root = []
|
||||
输出:[]
|
||||
```
|
||||
|
||||
## 思路推导
|
||||
|
||||
### 什么是翻转二叉树?
|
||||
|
||||
**翻转定义**: 交换每个节点的左右子节点
|
||||
|
||||
```
|
||||
翻转前: 翻转后:
|
||||
4 4
|
||||
/ \ / \
|
||||
2 7 7 2
|
||||
/ \ / \ / \ / \
|
||||
1 3 6 9 9 6 3 1
|
||||
```
|
||||
|
||||
### 暴力解法分析
|
||||
|
||||
**思路**: 从根节点开始,递归交换每个节点的左右子节点
|
||||
|
||||
**观察翻转的性质**:
|
||||
1. 翻转整棵树 = 翻转左子树 + 翻转右子树 + 交换左右子节点
|
||||
2. 这是一个天然的递归问题
|
||||
|
||||
**递归思路**:
|
||||
```
|
||||
invertTree(root):
|
||||
1. 翻转左子树
|
||||
2. 翻转右子树
|
||||
3. 交换左右子节点
|
||||
```
|
||||
|
||||
**图解**:
|
||||
```
|
||||
原始树:
|
||||
4
|
||||
/ \
|
||||
2 7
|
||||
/ \ / \
|
||||
1 3 6 9
|
||||
|
||||
步骤1: 翻转子节点(2和7)
|
||||
├─ 翻转2的子树
|
||||
│ ├─ 翻转1 → 1(叶子节点,不变)
|
||||
│ └─ 翻转3 → 3(叶子节点,不变)
|
||||
│ └─ 交换1和3 → 2的子树变为 [3, 1]
|
||||
└─ 翻转7的子树
|
||||
├─ 翻转6 → 6(叶子节点,不变)
|
||||
└─ 翻转9 → 9(叶子节点,不变)
|
||||
└─ 交换6和9 → 7的子树变为 [9, 6]
|
||||
|
||||
步骤2: 交换2和7
|
||||
最终结果:
|
||||
4
|
||||
/ \
|
||||
7 2
|
||||
/ \ / \
|
||||
9 6 3 1
|
||||
```
|
||||
|
||||
**时间复杂度**: O(n) - 每个节点访问一次
|
||||
**空间复杂度**: O(h) - h为树高,递归栈空间
|
||||
|
||||
### 为什么这样思考?
|
||||
|
||||
**核心思想**:
|
||||
1. **分治**: 大问题分解为小问题(翻转整棵树 → 翻转子树)
|
||||
2. **递归定义**: 翻转操作可以递归应用到子树上
|
||||
3. **原地操作**: 直接交换左右指针,不需要额外空间
|
||||
|
||||
**为什么先翻转再交换?**
|
||||
- 也可以先交换再翻转
|
||||
- 两种顺序都可以,结果相同
|
||||
- 但先翻转再交换更直观
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 递归 / 迭代
|
||||
### 核心思想
|
||||
从根节点开始,递归翻转左右子树,然后交换左右子节点。
|
||||
|
||||
## Go 代码(递归)
|
||||
### 详细算法流程
|
||||
|
||||
**步骤1: 处理基准情况**
|
||||
```go
|
||||
if root == nil {
|
||||
return nil // 空树直接返回
|
||||
}
|
||||
```
|
||||
|
||||
**关键点**: 递归的终止条件
|
||||
|
||||
**步骤2: 递归翻转左右子树**
|
||||
```go
|
||||
left := invertTree(root.Left) // 翻转左子树
|
||||
right := invertTree(root.Right) // 翻转右子树
|
||||
```
|
||||
|
||||
**关键点**: 先递归处理子节点,再处理当前节点
|
||||
|
||||
**步骤3: 交换左右子节点**
|
||||
```go
|
||||
root.Left = right
|
||||
root.Right = left
|
||||
```
|
||||
|
||||
**简化写法**:
|
||||
```go
|
||||
root.Left, root.Right = root.Right, root.Left
|
||||
```
|
||||
|
||||
**步骤4: 返回当前节点**
|
||||
```go
|
||||
return root
|
||||
```
|
||||
|
||||
**图解**:
|
||||
```
|
||||
翻转过程:
|
||||
|
||||
4 4 4
|
||||
/ \ / \ / \
|
||||
2 7 → 2 7 → 7 2
|
||||
/ \ / \ / \ / \ / \ / \
|
||||
1 3 6 9 3 1 9 6 9 6 3 1
|
||||
|
||||
原始 翻转子节点 交换子节点
|
||||
```
|
||||
|
||||
### 关键细节说明
|
||||
|
||||
**细节1: 为什么可以一行代码完成?**
|
||||
|
||||
```go
|
||||
root.Left, root.Right = invertTree(root.Right), invertTree(root.Left)
|
||||
```
|
||||
|
||||
**原因**: Go支持多值赋值,右侧的表达式先求值,再赋值给左侧
|
||||
|
||||
**执行顺序**:
|
||||
1. 计算 `invertTree(root.Right)`,得到翻转后的右子树
|
||||
2. 计算 `invertTree(root.Left)`,得到翻转后的左子树
|
||||
3. 同时赋值:`root.Left = 翻转后的右子树`,`root.Right = 翻转后的左子树`
|
||||
|
||||
**细节2: 为什么不需要返回值?**
|
||||
|
||||
实际上需要返回值!
|
||||
```go
|
||||
func invertTree(root *TreeNode) *TreeNode {
|
||||
if root == nil {
|
||||
return nil // 必须返回
|
||||
}
|
||||
// ...
|
||||
return root // 必须返回
|
||||
}
|
||||
```
|
||||
|
||||
**原因**:
|
||||
- 需要返回翻转后的根节点
|
||||
- 即使是原地操作,也需要返回值以保持接口一致
|
||||
|
||||
**细节3: 为什么是原地操作?**
|
||||
|
||||
```go
|
||||
// 直接修改指针,不需要创建新节点
|
||||
root.Left, root.Right = root.Right, root.Left
|
||||
```
|
||||
|
||||
**原因**:
|
||||
- 翻转操作只改变指针指向
|
||||
- 不需要创建新的节点
|
||||
- 空间复杂度只有递归栈
|
||||
|
||||
### 边界条件分析
|
||||
|
||||
**边界1: 空树**
|
||||
```
|
||||
输入: root = nil
|
||||
输出: nil
|
||||
处理: 直接返回nil
|
||||
```
|
||||
|
||||
**边界2: 只有根节点**
|
||||
```
|
||||
输入: root = [1]
|
||||
输出: [1]
|
||||
处理:
|
||||
- 翻转左子树 → nil
|
||||
- 翻转右子树 → nil
|
||||
- 交换nil和nil → 不变
|
||||
```
|
||||
|
||||
**边界3: 只有左子树**
|
||||
```
|
||||
输入:
|
||||
1
|
||||
/
|
||||
2
|
||||
|
||||
输出:
|
||||
1
|
||||
\
|
||||
2
|
||||
|
||||
处理:
|
||||
- 翻转2 → 2(叶子节点)
|
||||
- 翻转nil → nil
|
||||
- 交换 → 2变成右子树
|
||||
```
|
||||
|
||||
**边界4: 只有右子树**
|
||||
```
|
||||
输入:
|
||||
1
|
||||
\
|
||||
2
|
||||
|
||||
输出:
|
||||
1
|
||||
/
|
||||
2
|
||||
|
||||
处理:
|
||||
- 翻转nil → nil
|
||||
- 翻转2 → 2(叶子节点)
|
||||
- 交换 → 2变成左子树
|
||||
```
|
||||
|
||||
**边界5: 完全二叉树**
|
||||
```
|
||||
输入:
|
||||
1
|
||||
/ \
|
||||
2 3
|
||||
/ \ / \
|
||||
4 5 6 7
|
||||
|
||||
输出:
|
||||
1
|
||||
/ \
|
||||
3 2
|
||||
/ \ / \
|
||||
7 6 5 4
|
||||
```
|
||||
|
||||
### 复杂度分析(详细版)
|
||||
|
||||
**时间复杂度**:
|
||||
```
|
||||
- 每个节点访问一次: O(n)
|
||||
- 每次访问常数操作: O(1)
|
||||
- 总计: O(n)
|
||||
|
||||
为什么是O(n)?
|
||||
- 递归遍历所有节点
|
||||
- 每个节点只处理一次(交换左右)
|
||||
- 没有重复访问
|
||||
```
|
||||
|
||||
**空间复杂度**:
|
||||
```
|
||||
- 递归栈: O(h) - h为树高
|
||||
- 最坏情况(链状树): O(n)
|
||||
- 最好情况(完全平衡树): O(log n)
|
||||
- 总计: O(h)
|
||||
|
||||
为什么是O(h)而不是O(n)?
|
||||
- 递归深度 = 树的高度
|
||||
- 同一时刻栈中最多有h个栈帧
|
||||
- 不是所有节点同时在栈中
|
||||
```
|
||||
|
||||
### 执行过程演示
|
||||
|
||||
**输入**:
|
||||
```
|
||||
4
|
||||
/ \
|
||||
2 7
|
||||
/ \
|
||||
1 3
|
||||
```
|
||||
|
||||
**执行过程**:
|
||||
```
|
||||
调用 invertTree(4):
|
||||
├─ 调用 invertTree(2):
|
||||
│ ├─ 调用 invertTree(1):
|
||||
│ │ ├─ 1.Left = nil, 1.Right = nil
|
||||
│ │ └─ 返回 1
|
||||
│ ├─ 调用 invertTree(3):
|
||||
│ │ ├─ 3.Left = nil, 3.Right = nil
|
||||
│ │ └─ 返回 3
|
||||
│ ├─ 交换: 2.Left = 3, 2.Right = 1
|
||||
│ └─ 返回 2
|
||||
├─ 调用 invertTree(7):
|
||||
│ ├─ 7.Left = nil, 7.Right = nil
|
||||
│ └─ 返回 7
|
||||
├─ 交换: 4.Left = 7, 4.Right = 2
|
||||
└─ 返回 4
|
||||
|
||||
最终结果:
|
||||
4
|
||||
/ \
|
||||
7 2
|
||||
/ \
|
||||
3 1
|
||||
```
|
||||
|
||||
## 代码实现
|
||||
|
||||
### 方法一:递归(推荐)
|
||||
|
||||
```go
|
||||
func invertTree(root *TreeNode) *TreeNode {
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// 递归翻转左右子树并交换
|
||||
root.Left, root.Right = invertTree(root.Right), invertTree(root.Left)
|
||||
|
||||
return root
|
||||
}
|
||||
```
|
||||
|
||||
**复杂度:** O(n) 时间,O(h) 空间
|
||||
**复杂度**: O(n) 时间,O(h) 空间
|
||||
|
||||
### 方法二:递归(分步写法)
|
||||
|
||||
```go
|
||||
func invertTree(root *TreeNode) *TreeNode {
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 先翻转左右子树
|
||||
left := invertTree(root.Left)
|
||||
right := invertTree(root.Right)
|
||||
|
||||
// 再交换
|
||||
root.Left = right
|
||||
root.Right = left
|
||||
|
||||
return root
|
||||
}
|
||||
```
|
||||
|
||||
**复杂度**: O(n) 时间,O(h) 空间
|
||||
|
||||
### 方法三:迭代(栈)
|
||||
|
||||
```go
|
||||
func invertTree(root *TreeNode) *TreeNode {
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
stack := []*TreeNode{root}
|
||||
|
||||
for len(stack) > 0 {
|
||||
node := stack[len(stack)-1]
|
||||
stack = stack[:len(stack)-1]
|
||||
|
||||
// 交换左右子节点
|
||||
node.Left, node.Right = node.Right, node.Left
|
||||
|
||||
// 将子节点入栈
|
||||
if node.Left != nil {
|
||||
stack = append(stack, node.Left)
|
||||
}
|
||||
if node.Right != nil {
|
||||
stack = append(stack, node.Right)
|
||||
}
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
```
|
||||
|
||||
**复杂度**: O(n) 时间,O(n) 空间
|
||||
|
||||
### 方法四:迭代(队列 - BFS)
|
||||
|
||||
```go
|
||||
func invertTree(root *TreeNode) *TreeNode {
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
queue := []*TreeNode{root}
|
||||
|
||||
for len(queue) > 0 {
|
||||
node := queue[0]
|
||||
queue = queue[1:]
|
||||
|
||||
// 交换左右子节点
|
||||
node.Left, node.Right = node.Right, node.Left
|
||||
|
||||
// 将子节点入队
|
||||
if node.Left != nil {
|
||||
queue = append(queue, node.Left)
|
||||
}
|
||||
if node.Right != nil {
|
||||
queue = append(queue, node.Right)
|
||||
}
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
```
|
||||
|
||||
**复杂度**: O(n) 时间,O(n) 空间
|
||||
|
||||
## 常见错误
|
||||
|
||||
### 错误1: 忘记返回节点
|
||||
|
||||
❌ **错误写法**:
|
||||
```go
|
||||
func invertTree(root *TreeNode) *TreeNode {
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
root.Left, root.Right = invertTree(root.Right), invertTree(root.Left)
|
||||
// 忘记返回 root
|
||||
}
|
||||
```
|
||||
|
||||
✅ **正确写法**:
|
||||
```go
|
||||
func invertTree(root *TreeNode) *TreeNode {
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
root.Left, root.Right = invertTree(root.Right), invertTree(root.Left)
|
||||
return root // 必须返回
|
||||
}
|
||||
```
|
||||
|
||||
**原因**: 函数签名要求返回 `*TreeNode`
|
||||
|
||||
### 错误2: 交换后忘记处理子节点
|
||||
|
||||
❌ **错误写法**:
|
||||
```go
|
||||
func invertTree(root *TreeNode) *TreeNode {
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
root.Left, root.Right = root.Right, root.Left
|
||||
return root // 错误:只交换了当前节点
|
||||
}
|
||||
```
|
||||
|
||||
✅ **正确写法**:
|
||||
```go
|
||||
func invertTree(root *TreeNode) *TreeNode {
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
root.Left, root.Right = invertTree(root.Right), invertTree(root.Left)
|
||||
return root // 递归处理子节点
|
||||
}
|
||||
```
|
||||
|
||||
**原因**: 需要递归翻转所有子节点,不只是当前节点
|
||||
|
||||
### 错误3: 多值赋值理解错误
|
||||
|
||||
❌ **错误理解**:
|
||||
```go
|
||||
// 错误理解:先赋值 root.Left,再赋值 root.Right
|
||||
root.Left, root.Right = root.Right, root.Left
|
||||
```
|
||||
|
||||
**正确理解**:
|
||||
```go
|
||||
// Go的多值赋值是同时的
|
||||
// 右侧先求值,再同时赋值给左侧
|
||||
temp1, temp2 := root.Right, root.Left // 先求值
|
||||
root.Left = temp1 // 再赋值
|
||||
root.Right = temp2
|
||||
```
|
||||
|
||||
## 变体问题
|
||||
|
||||
### 变体1: 判断是否是翻转二叉树
|
||||
|
||||
判断两棵树是否互为翻转:
|
||||
|
||||
```go
|
||||
func isFlipTree(a, b *TreeNode) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
if a.Val != b.Val {
|
||||
return false
|
||||
}
|
||||
// a的左等于b的右,a的右等于b的左
|
||||
return isFlipTree(a.Left, b.Right) &&
|
||||
isFlipTree(a.Right, b.Left)
|
||||
}
|
||||
```
|
||||
|
||||
### 变体2: 翻转N叉树
|
||||
|
||||
```go
|
||||
type Node struct {
|
||||
Val int
|
||||
Children []*Node
|
||||
}
|
||||
|
||||
func invertNTree(root *Node) *Node {
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 翻转子节点列表
|
||||
for i, j := 0, len(root.Children)-1; i < j; i, j = i+1, j-1 {
|
||||
root.Children[i], root.Children[j] = root.Children[j], root.Children[i]
|
||||
}
|
||||
|
||||
// 递归翻转每个子节点
|
||||
for _, child := range root.Children {
|
||||
invertNTree(child)
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
```
|
||||
|
||||
### 变体3: 镜像对称二叉树
|
||||
|
||||
实际上就是翻转二叉树的应用:
|
||||
|
||||
```go
|
||||
func isSymmetric(root *TreeNode) bool {
|
||||
if root == nil {
|
||||
return true
|
||||
}
|
||||
return isMirror(root.Left, root.Right)
|
||||
}
|
||||
|
||||
func isMirror(left, right *TreeNode) bool {
|
||||
if left == nil && right == nil {
|
||||
return true
|
||||
}
|
||||
if left == nil || right == nil {
|
||||
return false
|
||||
}
|
||||
if left.Val != right.Val {
|
||||
return false
|
||||
}
|
||||
return isMirror(left.Left, invertTree(right.Right)) &&
|
||||
isMirror(left.Right, invertTree(right.Left))
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
**核心要点**:
|
||||
1. **递归定义**: 翻转整棵树 = 翻转子树 + 交换左右
|
||||
2. **原地操作**: 直接交换指针,不需要额外空间
|
||||
3. **多值赋值**: Go的多值赋值右侧先求值,再同时赋值
|
||||
4. **必须返回**: 即使是原地操作,也要返回根节点
|
||||
|
||||
**易错点**:
|
||||
- 忘记返回根节点
|
||||
- 只交换当前节点,忘记递归处理子节点
|
||||
- 多值赋值的理解错误
|
||||
|
||||
**方法选择**:
|
||||
- 递归法(推荐):代码简洁,逻辑清晰
|
||||
- 迭代法:避免递归栈溢出
|
||||
|
||||
**有趣的事实**: 这道题有个著名的故事——Max Howell(Homebrew作者)说Google面试官因为这道题拒绝了他,但这道题确实很简单!😄
|
||||
|
||||
Reference in New Issue
Block a user