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

605 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.
# 翻转二叉树 (Invert Binary Tree)
LeetCode 226. 简单
## 题目描述
给你一棵二叉树的根节点 `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. **原地操作**: 直接交换左右指针,不需要额外空间
**为什么先翻转再交换?**
- 也可以先交换再翻转
- 两种顺序都可以,结果相同
- 但先翻转再交换更直观
## 解题思路
### 核心思想
从根节点开始,递归翻转左右子树,然后交换左右子节点。
### 详细算法流程
**步骤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) 空间
### 方法二:递归(分步写法)
```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 HowellHomebrew作者说Google面试官因为这道题拒绝了他但这道题确实很简单😄