# 二叉树的中序遍历 (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] 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) 使用队列而非栈: ```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`) **推荐写法**: 迭代法(空间可控,面试常考)