按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度: 1. 二叉树的中序遍历 - 增加"思路推导"部分,解释递归到迭代的转换 - 详细说明迭代法的每个步骤 - 增加执行过程演示和多种解法 2. 二叉树的最大深度 - 增加"思路推导",对比DFS和BFS - 详细解释递归的基准情况 - 增加多种解法和变体问题 3. 从前序与中序遍历序列构造二叉树 - 详细解释前序和中序的特点 - 增加"思路推导",说明如何分治 - 详细说明切片边界计算 4. 对称二叉树 - 解释镜像对称的定义 - 详细说明递归比较的逻辑 - 增加迭代解法和变体问题 5. 翻转二叉树 - 解释翻转的定义和过程 - 详细说明多值赋值的执行顺序 - 增加多种解法和有趣的故事 6. 路径总和 - 详细解释路径和叶子节点的定义 - 说明为什么使用递减而非累加 - 增加多种解法和变体问题 每个文件都包含: - 完整的示例和边界条件分析 - 详细的算法流程和图解 - 关键细节说明 - 常见错误分析 - 复杂度分析(详细版) - 执行过程演示 - 多种解法 - 变体问题 - 总结 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
12 KiB
二叉树的中序遍历 (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]
↑ ↑ ↑
左 根 右
暴力解法分析
递归解法 - 最直观的思路
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
为什么这样思考?
- 模拟递归的调用栈
- 利用栈的"后进先出"特性
- 先保存路径,再反向访问
解题思路
方法一:递归(直观但空间开销大)
核心思想
按照"左→根→右"的顺序递归访问节点。
算法流程
步骤1: 定义递归函数
func inorder(node *TreeNode, result *[]int)
步骤2: 递归终止条件
if node == nil {
return // 空节点直接返回
}
步骤3: 按顺序递归
// 1. 先遍历左子树
inorder(node.Left, result)
// 2. 再访问根节点
*result = append(*result, node.Val)
// 3. 最后遍历右子树
inorder(node.Right, result)
方法二:迭代(栈模拟递归)
核心思想
用显式栈替代递归调用栈,模拟递归的执行过程。
详细算法流程
步骤1: 初始化数据结构
result := []int{} // 存储遍历结果
stack := []*TreeNode{} // 模拟递归栈
curr := root // 当前访问的节点
步骤2: 外层循环 - 只要还有节点可访问
for curr != nil || len(stack) > 0 {
// ...
}
关键点:
curr != nil: 还有节点需要处理len(stack) > 0: 栈中还有未访问的节点- 两个条件满足一个就可以继续
步骤3: 内层循环 - 一直往左走,找到最左节点
for curr != nil {
stack = append(stack, curr) // 路径上的节点都入栈
curr = curr.Left // 继续往左走
}
图解:
1
/ \
2 3
/ \
4 5
第一次内层循环后:
stack: [1, 2, 4] <- 从根到最左的路径
curr: nil <- 4的左子树为空
步骤4: 出栈并访问
curr = stack[len(stack)-1] // 取栈顶元素
stack = stack[:len(stack)-1] // 出栈
result = append(result, curr.Val) // 访问该节点
图解:
出栈4并访问:
stack: [1, 2]
result: [4]
curr: 4
步骤5: 转向右子树
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?
// ❌ 错误写法
for curr != nil { // 会漏掉栈中剩余的节点
// ✅ 正确写法
for curr != nil || len(stack) > 0 {
原因:
curr == nil时,栈中可能还有节点len(stack) == 0时,curr可能指向某个右子树- 两个条件需要同时考虑
细节2: 为什么是 curr = curr.Right 而不是继续往左?
// 出栈并访问后
result = append(result, curr.Val)
curr = curr.Right // 转向右子树
原因: 中序遍历顺序是"左→根→右"
- 访问完根节点后,该访问右子树了
- 右子树也要按"左→根→右"遍历
- 下次循环会从右子树的最左节点开始
细节3: 为什么内层循环要一直往左走?
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]
代码实现
方法一:递归
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) 空间
方法二:迭代(推荐)
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))
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: 循环条件错误
❌ 错误写法:
for curr != nil { // 漏掉了栈中还有节点的情况
// ...
}
✅ 正确写法:
for curr != nil || len(stack) > 0 {
// ...
}
原因: 当 curr == nil 时,栈中可能还有未访问的节点
错误2: 出栈后忘记转向右子树
❌ 错误写法:
curr = stack[len(stack)-1]
stack = stack[:len(stack)-1]
result = append(result, curr.Val)
// 缺少这一行: curr = curr.Right
✅ 正确写法:
curr = stack[len(stack)-1]
stack = stack[:len(stack)-1]
result = append(result, curr.Val)
curr = curr.Right // 重要:转向右子树
原因: 访问完根节点后,必须访问右子树
错误3: 内层循环条件错误
❌ 错误写法:
for curr.Left != nil { // 错误:会导致最左节点不入栈
stack = append(stack, curr)
curr = curr.Left
}
✅ 正确写法:
for curr != nil {
stack = append(stack, curr)
curr = curr.Left
}
原因: 最左节点也需要入栈,然后出栈访问
变体问题
变体1: 前序遍历
顺序: 根 → 左 → 右
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: 后序遍历
顺序: 左 → 右 → 根
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)
使用队列而非栈:
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
}
总结
核心要点:
- 中序遍历顺序: 左 → 根 → 右
- 迭代法核心: 用栈模拟递归调用
- 关键操作: 一直往左 → 出栈访问 → 转向右边
- 循环条件:
curr != nil || len(stack) > 0
易错点:
- 循环条件容易漏掉栈的情况
- 出栈后忘记转向右子树
- 内层循环条件错误(应该用
curr != nil)
推荐写法: 迭代法(空间可控,面试常考)