从「最大子数组和」的 Kadane 算法中回过神来,我们正式踏入一个全新的领域——二叉树。
如果说数组和链表是线性的“一维世界”,那么二叉树就是充满分支的“二维世界”。它是后续众多高级数据结构(如二叉搜索树、堆、平衡树)的基石。而理解二叉树的起点,就是掌握它的四种基本遍历方式。
很多人觉得遍历很简单,递归几行就搞定了。但当面试官追问“如何用迭代实现?”或者“如何统一三种遍历的迭代写法?”时,很多人就卡住了。今天就让我们从最直观的递归开始,一步步深入到迭代,最终抵达那个让人眼前一亮的“统一迭代法”。
📖 二叉树的四种遍历方式
在开始之前,我们先明确这四种遍历方式的定义:
- 前序遍历:根 -> 左子树 -> 右子树
- 中序遍历:左子树 -> 根 -> 右子树
- 后序遍历:左子树 -> 右子树 -> 根
- 层序遍历:从上到下、从左到右,逐层访问
第一层:递归法 —— 最直观的“分而治之”
递归是理解树结构最自然的方式。只要按照遍历的顺序,在递归函数的不同位置处理当前节点即可。
// 递归三要素
class Solution {
// 1. 确定递归函数的参数和返回值
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
// 2. 确定终止条件
preorder(root, res);
return res;
}
void preorder(TreeNode node, List<Integer> res) {
if (node == null) return; // 终止条件
// 3. 确定单层递归的逻辑
res.add(node.val); // 前序:处理根
preorder(node.left, res);
// 中序:处理根 (res.add(node.val);)
preorder(node.right, res);
// 后序:处理根 (res.add(node.val);)
}
}
- 复杂度:时间 O(n),空间 O(n)(递归栈深度)。
第二层:迭代法(标准版) —— 手动模拟递归栈
递归是系统帮我们维护了一个栈。迭代则是我们手动维护一个栈来模拟这个过程。
前序遍历(最简单)
前序遍历的顺序是“根-左-右”。我们可以利用栈的“先进后出”特性,先压入右孩子,再压入左孩子,这样出栈的顺序就是“左-右”。
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) return res;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
res.add(node.val); // 处理根节点
// 先压右,再压左,保证左子树先被处理
if (node.right != null) stack.push(node.right);
if (node.left != null) stack.push(node.left);
}
return res;
}
}
中序遍历(需一路向左)
中序遍历“左-根-右”的逻辑是:需要一直找到最左的节点,然后再处理根和右子树。
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
// 1. 一路向左,把所有左节点压入栈
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
// 2. 弹出最左节点,处理它(根)
cur = stack.pop();
res.add(cur.val);
// 3. 转向右子树
cur = cur.right;
}
return res;
}
}
后序遍历(最烧脑)
后序“左-右-根”是最复杂的。一个技巧是:前序是“根-左-右”,我们稍作修改得到“根-右-左”,再反转结果就是“左-右-根”。
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) return res;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
// 1. 按照“根-右-左”的顺序遍历
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
res.add(node.val);
if (node.left != null) stack.push(node.left);
if (node.right != null) stack.push(node.right);
}
// 2. 反转结果,得到“左-右-根”
Collections.reverse(res);
return res;
}
}
第三层:迭代法(统一版) —— “一套代码走天下”
上面三种迭代法的逻辑各不相同,容易记混。有一种更优雅的统一写法:在栈中同时存储节点和一个空标记,用来标识该节点是否应该被处理。
核心思想:如果从栈中弹出的是节点,说明它还没被处理,就按照逆序将其和孩子节点压回栈中,并在节点后插入一个null标记。如果弹出的是null,说明下一个节点该被处理了。
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) return res;
Stack<Object> stack = new Stack<>(); // 用Object类型来容纳TreeNode和null
stack.push(root);
while (!stack.isEmpty()) {
Object obj = stack.pop();
if (obj instanceof TreeNode) {
TreeNode node = (TreeNode) obj;
// 前序:顺序是 根-左-右,入栈顺序应为 右-左-根
if (node.right != null) stack.push(node.right);
if (node.left != null) stack.push(node.left);
stack.push(node); // 当前节点
stack.push(null); // 标记
} else {
// 遇到null标记,下一个节点就是要处理的节点
res.add(((TreeNode) stack.pop()).val);
}
}
return res;
}
}
改变遍历顺序只需调整入栈顺序:
- 前序(根-左-右):入栈顺序
右 -> 左 -> 根 - 中序(左-根-右):入栈顺序
右 -> 根 -> 左 - 后序(左-右-根):入栈顺序
根 -> 右 -> 左
这种方式极大地降低了记忆成本,理解了这一种,就等于理解了三种。
第四层:层序遍历(BFS) —— 用“队列”逐层推进
以上三种都属于深度优先搜索(DFS),而层序遍历是广度优先搜索(BFS)。
它利用**队列(Queue)**的先进先出特性,像水波一样一圈圈地向外扩散。关键是记录每一层的节点数 size。
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) return res;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size(); // 关键:记录当前层的节点数
List<Integer> level = new ArrayList<>();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
res.add(level);
}
return res;
}
}
💎 总结
| 遍历方式 | 核心数据结构 | 特点 | 应用场景 |
|---|---|---|---|
| 前序遍历 | 栈 (Stack) | 根节点最先被访问 | 复制二叉树、求二叉树深度 |
| 中序遍历 | 栈 (Stack) | 结果有序(对BST而言) | 验证二叉搜索树(BST) |
| 后序遍历 | 栈 (Stack) | 根节点最后被访问 | 删除二叉树、计算树的高度 |
| 层序遍历 | 队列 (Queue) | 按层处理,BFS思想 | 求树的宽度、找最底层最左节点 |
注:递归法最直观,但迭代法更考验对数据结构的理解,且无栈溢出风险。
掌握了这四种遍历方式,就等于拿到了操作二叉树的第一把钥匙。下一站,我们将带着这些工具,去挑战力扣的一道经典题目:「从前序与中序遍历序列构造二叉树」(LC 105),看看如何利用遍历顺序反推出一棵树的结构。不见不散!🚀