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

12 KiB
Raw Blame History

二叉树的中序遍历 (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. 递归调用就是压栈
  2. 递归返回就是出栈
  3. 中序遍历先找最左节点,再逐层返回

为什么迭代法这样写?

核心思想:

  1. 一直往左走,把路径上的节点都入栈
  2. 到达最左节点后,出栈并访问
  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) == 0curr可能指向某个右子树
  • 两个条件需要同时考虑

细节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
}

总结

核心要点:

  1. 中序遍历顺序: 左 → 根 → 右
  2. 迭代法核心: 用栈模拟递归调用
  3. 关键操作: 一直往左 → 出栈访问 → 转向右边
  4. 循环条件: curr != nil || len(stack) > 0

易错点:

  • 循环条件容易漏掉栈的情况
  • 出栈后忘记转向右子树
  • 内层循环条件错误(应该用 curr != nil

推荐写法: 迭代法(空间可控,面试常考)