二叉树前中后序遍历统一迭代法详解:空标记法与栈操作的艺术
二叉树的 前序、中序、后序 遍历是算法中的经典问题。递归实现简单直观,而迭代法则能更好地理解栈的操作逻辑。前文中(中序,前序与后序)所讲过传统的迭代法需要为每种遍历设计不同的入栈顺序,但 统一迭代法 通过引入 空标记节点,将三种遍历的代码结构统一,极大降低了记忆难度。本文将结合代码与出入栈模拟,深入解析这一技巧的核心思想。
一、统一迭代法的核心思想
1. 为什么需要空标记?
在传统迭代法中,前序、中序、后序遍历的栈操作逻辑差异较大,难以统一。空标记法 的核心在于将 节点的访问 和 值的记录 分离:
- 访问节点:将节点按遍历顺序的逆序压入栈(例如前序需先处理根,因此根先入栈)。
- 记录值:当遇到空标记时,表示当前栈顶节点已完成子节点处理,可以记录其值。
2. 统一操作步骤
- 节点入栈:按遍历顺序的逆序压入节点及其子节点。
- 插入空标记:在需要记录值的节点后压入
null
作为标记。 - 处理标记:当栈顶为
null
时,弹出null
并记录下一个节点的值。
二、前序遍历
1. 代码解析
class Solution {public List<Integer> preorderTraversal(TreeNode root) {List<Integer> res = new ArrayList<>();Stack<TreeNode> stack = new Stack<>();if (root == null) return res;stack.push(root);while (!stack.isEmpty()) {TreeNode node = stack.peek();if (node != null) {stack.pop();// 前序逆序入栈:根 → 右 → 左 → null标记 → 根if (node.right != null) stack.push(node.right);if (node.left != null) stack.push(node.left);stack.push(node);stack.push(null); // 插入空标记} else {stack.pop(); // 弹出 nullnode = stack.pop();res.add(node.val);}}return res;}
}
2. 出入栈模拟(以 [1,2,3]
为例)
栈内容(底→顶) | 操作 | 结果 res |
---|---|---|
[1] | 初始状态 | [] |
[1.right(3),1.left(2),1,null] | 弹出 1,按右左顺序重新入栈 | [] |
[3,2,1,null] | 处理非空节点 1 | [] |
[3,2,1,null] → 弹出 null | 遇到 null,记录 1 | [1] |
[3,2] | 处理 2 | [1] |
[3,2.right(null),2.left(null),2,null] | 弹出 2,入栈子节点 | [1] |
[3,2,null] → 弹出 null | 记录 2 | [1,2] |
[3] | 处理 3 | [1,2] |
[3.right(null),3.left(null),3,null] | 弹出 3,入栈子节点 | [1,2] |
[3,null] → 弹出 null | 记录 3 | [1,2,3] |
三、中序遍历
1. 代码解析
class Solution {public List<Integer> inorderTraversal(TreeNode root) {List<Integer> res = new ArrayList<>();Stack<TreeNode> stack = new Stack<>();if (root == null) return res;stack.push(root);while (!stack.isEmpty()) {TreeNode node = stack.peek();if (node != null) {stack.pop();// 中序逆序入栈:右 → 根 → null标记 → 左if (node.right != null) stack.push(node.right);stack.push(node);stack.push(null);if (node.left != null) stack.push(node.left);} else {stack.pop(); // 弹出 nullnode = stack.pop();res.add(node.val);}}return res;}
}
2. 出入栈模拟(以 [1,2,3]
为例)
栈内容(底→顶) | 操作 | 结果 res |
---|---|---|
[1] | 初始状态 | [] |
[1.right(3),1,null,1.left(2)] | 弹出 1,按右根左顺序入栈 | [] |
[3,1,null,2] | 处理非空节点 2 | [] |
[3,1,null,2.right(null),2,null,2.left(null)] | 弹出 2 | [] |
[3,1,null,2,null] → 弹出 null | 记录 2 | [2] |
[3,1,null] → 弹出 null | 记录 1 | [2,1] |
[3] | 处理 3 | [2,1] |
[3.right(null),3,null,3.left(null)] | 弹出 3 | [2,1] |
[3,null] → 弹出 null | 记录 3 | [2,1,3] |
四、后序遍历
1. 代码解析
class Solution {public List<Integer> postorderTraversal(TreeNode root) {List<Integer> res = new ArrayList<>();Stack<TreeNode> stack = new Stack<>();if (root == null) return res;stack.push(root);while (!stack.isEmpty()) {TreeNode node = stack.peek();if (node != null) {stack.pop();// 后序逆序入栈:根 → null标记 → 右 → 左stack.push(node);stack.push(null);if (node.right != null) stack.push(node.right);if (node.left != null) stack.push(node.left);} else {stack.pop(); // 弹出 nullnode = stack.pop();res.add(node.val);}}return res;}
}
2. 出入栈模拟(以 [1,2,3]
为例)
栈内容(底→顶) | 操作 | 结果 res |
---|---|---|
[1] | 初始状态 | [] |
[1,null,1.right(3),1.left(2)] | 弹出 1,按根右左顺序入栈 | [] |
[null,3,2] | 处理非空节点 2 | [] |
[null,3,2.right(null),2,null,2.left(null)] | 弹出 2 | [] |
[null,3,2,null] → 弹出 null | 记录 2 | [2] |
[null,3] → 弹出 null | 记录 3 | [2,3] |
[null] → 弹出 null | 记录 1 | [2,3,1] |
五、关键知识点总结
1. 统一迭代法的优势
- 代码结构统一:三种遍历的代码逻辑高度相似,只需调整入栈顺序。
- 逻辑清晰:通过空标记明确区分节点的访问和值记录阶段。
2. 入栈顺序的规律
遍历方式 | 入栈顺序(逆序) | 实际遍历顺序 |
---|---|---|
前序 | 右 → 左 → 根 → null | 根 → 左 → 右 |
中序 | 右 → 根 → null → 左 | 左 → 根 → 右 |
后序 | 根 → null → 右 → 左 | 左 → 右 → 根 |
3. 复杂度分析
- 时间复杂度:O(n),每个节点入栈和出栈两次。
- 空间复杂度:O(n),栈存储所有节点。
六、总结
统一迭代法的核心在于 空标记 的引入,通过将节点的访问和值记录分离,实现了三种遍历的逻辑统一。关键点总结:
- 逆序入栈:按遍历顺序的逆序压入节点。
- 空标记触发记录:遇到
null
时记录栈顶节点的值。 - 调整顺序即可切换遍历方式:只需修改入栈顺序,无需重写逻辑。
掌握这一方法后,可以轻松应对二叉树遍历的所有变体问题!