二叉树的 层序遍历 是广度优先搜索(BFS)的经典应用,其核心是按层级逐层访问节点,确保每一层的节点按从左到右的顺序被处理。本文将结合代码实现与队列操作模拟,深入解析层序遍历的两大核心难点,并通过具体案例演示层级分组的关键逻辑。
一、题目描述
给定一个二叉树的根节点 root
,返回其节点值的 层序遍历 结果(即逐层遍历,从左到右访问所有节点)。
例如,输入二叉树:
3
/ \
9 20
/ \
15 7
输出结果为:
[[3], [9,20], [15,7]]
。
二、核心难点分析
难点1:出入队逻辑------如何区分当前层与下一层节点?
层序遍历的本质是通过队列实现 层级隔离,确保每一步处理的是同一层的节点,而子节点被暂存到队列中,作为下一层的待处理节点。
- 入队规则 :
每次处理当前层节点时,将其左右子节点按顺序入队(先左后右),保证下一层节点在队列中按从左到右的顺序排列。 - 出队规则 :
每次循环开始时,队列中存储的是当前层的所有节点,通过固定次数的出队操作(次数等于当前层节点数),确保不会提前处理下一层节点。
类比理解 :
队列就像一个"层级传送带":
- 第一层节点(根节点)入队后,传送带启动,每次取出当前层所有节点(出队),并将它们的子节点(下一层)按顺序放入传送带尾部(入队)。
- 传送带每"运行一轮",就处理完一层节点,且下一层节点已在传送带中等待,确保层级不会混淆。
难点2:循环压入结果数组的逻辑------如何避免跨层数据污染?
层序遍历需要将每一层的节点值单独收集到一个列表中,关键在于 通过当前层节点数控制循环范围:
- 在每层循环开始时,记录队列的大小
levelSize
,这个值就是当前层的节点总数(因为此时队列中仅包含当前层节点)。 - 内层循环强制执行
levelSize
次出队操作,确保只处理当前层的节点,即使在循环中向队列添加了下一层节点(子节点入队),也不会影响内层循环的次数(因为levelSize
是固定的)。
反例说明 :
如果不记录 levelSize
,直接通过 !queue.isEmpty()
控制内层循环,会导致下一层节点被提前处理,最终结果中不同层的节点会混合在一起(例如第二层节点和第三层节点被收集到同一个列表中)。
三、解题思路分步解析
步骤1:初始化队列与结果列表
java
List<List<Integer>> res = new ArrayList<>(); // 存储最终结果,每一层是一个子列表
Deque<TreeNode> queue = new ArrayDeque<>(); // 使用双端队列实现队列操作(Java中推荐用ArrayDeque)
if (root != null) {
queue.offer(root); // 根节点入队,作为第一层唯一的节点
}
- 为什么用Deque而非Queue?
Deque
(双端队列)提供了更高效的offer()
和poll()
操作,且在Java中PriorityQueue
不适合此处场景(层序遍历需要严格的FIFO顺序,无需优先级)。
步骤2:逐层处理节点(核心逻辑)
java
while (!queue.isEmpty()) {
int levelSize = queue.size(); // 关键!记录当前层的节点数,此时队列中只有当前层节点
List<Integer> levelList = new ArrayList<>(); // 临时存储当前层的节点值
// 内层循环:处理当前层的所有节点(严格执行levelSize次)
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll(); // 出队当前层节点(从队首取出,保证左到右顺序)
levelList.add(node.val); // 记录节点值
// 子节点入队(先左后右,保证下一层节点在队列中的顺序)
if (node.left != null) queue.offer(node.left); // 左子节点入队(队尾)
if (node.right != null) queue.offer(node.right); // 右子节点入队(队尾)
}
res.add(levelList); // 当前层处理完毕,结果加入最终列表
}
关键细节解析:
-
levelSize = queue.size()
的作用:- 在进入内层循环前,队列中存储的是当前层的所有节点(例如第一层只有根节点,第二层是根节点的左右子节点)。
- 这个值确保了内层循环只会处理当前层的节点,而后续入队的子节点(下一层)会被外层循环的下一次迭代处理。
-
子节点入队的顺序:
- 先左子节点后右子节点,保证下一层节点在队列中按从左到右的顺序排列,从而在内层循环中按顺序处理(左到右访问)。
四、出入队模拟示例(以样例二叉树为例)
二叉树结构:
3
/ \
9 20
/ \
15 7
队列变化与结果收集过程如下:
处理层级 | 队列内容(队首→队尾) | 内层循环操作 | 临时列表 levelList |
结果 res |
---|---|---|---|---|
第1层(根层) | [3] |
弹出3,左子节点9和右子节点20入队 | [3] |
[[3]] |
第2层 | [9, 20] |
弹出9(无子节点),弹出20(左15、右7入队) | [9, 20] |
[[3], [9, 20]] |
第3层 | [15, 7] |
弹出15(无子节点),弹出7(无子节点) | [15, 7] |
[[3], [9, 20], [15, 7]] |
- 关键观察 :
每次外层循环处理时,队列中始终只包含当前层的节点,子节点入队后成为下一次外层循环的处理对象,确保层级严格分离。
五、完整代码与复杂度分析
完整代码
java
import java.util.*;
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
Deque<TreeNode> queue = new ArrayDeque<>();
if (root != null) {
queue.offer(root);
}
while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> levelList = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
levelList.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
res.add(levelList);
}
return res;
}
}
复杂度分析
- 时间复杂度:O(n),每个节点恰好入队和出队一次,内层循环总次数等于节点总数n。
- 空间复杂度:O(n),最坏情况下队列存储最后一层的所有节点(例如完全二叉树的最后一层有n/2个节点)。
六、扩展问题与变种题型
掌握层序遍历的核心逻辑后,可以轻松解决以下变种问题:
- 自底向上的层序遍历(LeetCode 107) :
- 只需将收集到的每层结果逆序添加到最终列表中。
- 锯齿形层序遍历(LeetCode 103) :
- 偶数层(从0开始计数)将临时列表反转,实现左右交替的访问顺序。
- 二叉树右视图(LeetCode 199) :
- 每层处理时,将最后一个节点的值加入结果(利用队列的FIFO特性,最后出队的节点是当前层最右边的节点)。
七、总结
层序遍历的核心是通过 队列的FIFO特性实现层级隔离 ,并通过 固定层节点数的循环 确保每层数据准确收集。理解这两大难点后,不仅能轻松解决基础层序遍历问题,还能快速应对各种变种题型。
- 出入队逻辑:当前层节点出队时,子节点按顺序入队,作为下一层的待处理节点,保证层级有序。
- 结果收集逻辑:通过记录当前层节点数,内层循环严格处理固定次数,避免跨层数据污染。
掌握这一经典BFS模式,将为解决树结构的层级相关问题打下坚实基础。