• 中文 • Walking in Code
二叉树中序遍历 - 递归、迭代与 Morris 解法完全指南
通过交互式可视化掌握二叉树中序遍历。学习递归、迭代(栈)和 Morris O(1) 空间解法。用完整的 Java、Python、JavaScript 模板解决 LeetCode 94。
#algorithm
#binary-tree
#tree-traversal
#inorder
#visualization
#interview
#morris-traversal
#bst
简介
中序遍历遵循左 → 根 → 右的模式:
- 遍历左子树
- 访问当前节点
- 遍历右子树
BST 的魔法 ✨
对于二叉搜索树(BST),中序遍历按升序排列访问节点!这是中序遍历最重要的特性。
BST: 4
/ \
2 6
/ \ / \
1 3 5 7
中序遍历: 1 → 2 → 3 → 4 → 5 → 6 → 7 (有序!)
为什么面试官喜欢考它
- 对 BST 问题至关重要(第 k 小、验证 BST 等)
- 测试对递归和栈操作的理解
- Morris 遍历展示空间优化的掌握程度
- 是许多高级树算法的基础
何时使用中序遍历
- BST 中第 k 小/大元素(LC 230)
- 验证 BST(LC 98)
- 将 BST 转换为有序列表
- 查找 BST 的 floor/ceiling
- 恢复 BST(LC 99)
直观理解
1
/ \
2 3
/ \
4 5
中序遍历: 4 → 2 → 5 → 1 → 3
(左→根→右)
访问顺序:
步骤 1: 向左到 2,再向左到 4
步骤 2: 访问 4(无子节点)
步骤 3: 回溯,访问 2
步骤 4: 向右,访问 5
步骤 5: 回溯到 1,访问 1
步骤 6: 向右,访问 3
交互式可视化
观察算法逐步遍历树的过程。比较递归、迭代和 Morris 方法!
Inorder (Left→Root→Right) - Iterative
Step 1 / 0INIT
Ready to start
Stack
Empty
Result
[]
Current
Visited
In Stack
解法一:递归方法
最直观的解法:向左走,访问,向右走。
代码模板
Loading...
工作原理
- 基本情况:如果节点为空,返回
- 递归左子树:先处理整个左子树
- 访问根节点:添加当前节点的值
- 递归右子树:然后处理右子树
调用栈自动处理”向上返回”的逻辑。
解法二:迭代方法(栈)
关键洞察:我们需要在访问任何节点之前尽可能向左走。
代码模板
Loading...
逐步演示
树结构: 1
/ \
2 3
/
4
步骤 1: 压入 1, 向左 栈: [1]
步骤 2: 压入 2, 向左 栈: [1, 2]
步骤 3: 压入 4, 向左 栈: [1, 2, 4]
步骤 4: 无左子节点, 弹出 4, 访问 结果: [4]
步骤 5: 无右子节点, 弹出 2, 访问 结果: [4, 2]
步骤 6: 向右(无), 弹出 1, 访问 结果: [4, 2, 1]
步骤 7: 向右到 3
步骤 8: 压入 3, 无左, 弹出 3, 访问 结果: [4, 2, 1, 3]
最终结果: [4, 2, 1, 3]
模式总结
while (current 存在 OR 栈非空):
1. 一直向左走(压入所有左节点)
2. 弹出并访问
3. 向右走(成为新的 current)
解法三:Morris 遍历(O(1) 空间)
Morris 遍历使用线索来消除栈。它临时创建从前驱节点回到后继节点的链接。
与前序遍历的关键区别
- 前序 Morris:创建线索之前访问
- 中序 Morris:通过线索返回之后访问
代码模板
Loading...
线索工作原理
原始树: 添加线索后:
4 4
/ \ / \
2 6 2 6
/ \ / \
1 3 1 3
\
→ (线索指向 4)
线索: 3 的右指针指向 4(其中序后继)
访问时机
对于中序遍历,我们在以下情况访问:
1. 无左子节点 → 访问,向右移动
2. 线索存在(返回时) → 访问,删除线索,向右移动
不是在创建线索时访问!那是前序遍历。
LeetCode 94:二叉树的中序遍历 (LC 94)
经典的中序遍历问题。三种解法都可以直接使用。
示例:
输入: root = [1,null,2,3]
1
\
2
/
3
输出: [1, 3, 2]
问题二:BST 中第 K 小的元素 (LC 230)
由于中序遍历对 BST 给出有序序列,只需返回第 k 个元素!
public int kthSmallest(TreeNode root, int k) {
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode current = root;
while (current != null || !stack.isEmpty()) {
while (current != null) {
stack.push(current);
current = current.left;
}
current = stack.pop();
if (--k == 0) return current.val; // 找到第 k 个!
current = current.right;
}
return -1; // k > 树的大小
}
问题三:验证二叉搜索树 (LC 98)
使用中序遍历并检查每个值是否大于前一个值:
def isValidBST(root: Optional[TreeNode]) -> bool:
stack = []
current = root
prev = float('-inf')
while current or stack:
while current:
stack.append(current)
current = current.left
current = stack.pop()
if current.val <= prev: # 必须严格递增
return False
prev = current.val
current = current.right
return True
复杂度分析
| 方法 | 时间 | 空间 | 说明 |
|---|---|---|---|
| 递归 | O(n) | O(h) | h = 树高,最坏 O(n) |
| 迭代 | O(n) | O(h) | 显式栈 |
| Morris | O(n) | O(1) | 每条边最多访问两次 |
空间对比
平衡树 斜树
O O
/ \ \
O O O
/ \ / \ \
O O O O O
栈深度: O(log n) 栈深度: O(n)
常见错误
1. 迭代:忘记外层条件
// 错误:只检查栈
while (!stack.isEmpty()) { // 漏掉了初始的 current
// ...
}
// 正确:两者都检查
while (current != null || !stack.isEmpty()) {
// ...
}
2. Morris:访问时机错误
# 中序遍历错误:向左走之前访问
if not predecessor.right:
result.append(current.val) # 太早了!
predecessor.right = current
current = current.left
# 正确:返回时访问
if predecessor.right: # 线索存在
predecessor.right = None
result.append(current.val) # 现在访问!
current = current.right
3. 没有处理空树
// 总是先检查空根节点
function inorderTraversal(root) {
if (!root) return []; // 别忘了!
// ...
}
边界情况
- 空树:返回
[] - 单节点:返回
[root.val] - 左斜树:测试最大栈深度
- 右斜树:测试迭代逻辑
- 完全二叉树:标准平衡情况
面试技巧
如何解释中序遍历
- “中序意味着 左 → 根 → 右”
- “对于 BST,这给出有序序列”
- “栈模拟递归中的调用栈”
- “Morris 创建临时线索来节省空间”
后续问题
| 问题 | 答案 |
|---|---|
| ”为什么对 BST 使用中序?“ | 给出升序排列 |
| ”如何获得降序?“ | 右 → 根 → 左(反向中序) |
| “可以用 O(1) 空间吗?“ | 是的,Morris 遍历 |
| ”如果不能修改树呢?“ | 使用栈,O(h) 空间 |
与其他遍历的比较
| 属性 | 前序 | 中序 | 后序 |
|---|---|---|---|
| 顺序 | 根→左→右 | 左→根→右 | 左→右→根 |
| BST 有序? | 否 | 是 ✓ | 否 |
| 用例 | 复制树 | BST 操作 | 删除树 |
| 访问时机 | 子节点之前 | 子节点之间 | 子节点之后 |
总结
| 方面 | 递归 | 迭代 | Morris |
|---|---|---|---|
| 简单程度 | ⭐⭐⭐ | ⭐⭐ | ⭐ |
| 空间 | O(h) | O(h) | O(1) |
| 修改树 | 否 | 否 | 是(临时) |
| 最适合 | 学习 | 面试 | 优化 |
需要记住的关键模式:
- 中序 = 左 → 根 → 右
- BST + 中序 = 有序序列
- 迭代:一直向左压栈,弹出并访问,向右走
- Morris:线索存在时访问(不是创建时)
中序遍历对 BST 问题至关重要。掌握这三种实现方法!