Java版LeetCode热题100之二叉树的层序遍历:从BFS到多维拓展的全面解析
本文将深入剖析 LeetCode 第102题「二叉树的层序遍历」,不仅提供广度优先搜索(BFS)的标准解法,还涵盖算法原理、复杂度分析、面试技巧、工程应用及关联题目拓展。全文约9500字,结构完整、内容翔实,适合准备面试或夯实算法基础的开发者阅读。
一、原题回顾
题目编号:LeetCode 102
题目名称:Binary Tree Level Order Traversal(二叉树的层序遍历)
难度等级:Medium(基础但重要)
题目描述
给你二叉树的根节点root,返回其节点值的层序遍历。
层序遍历:逐层地,从左到右访问所有节点。
输出格式:返回一个列表的列表,其中每个子列表包含一层的节点值。
示例
示例 1:
输入:root = [3,9,20,null,null,15,7] 输出:[[3],[9,20],[15,7]]树结构如下:
3 / \ 9 20 / \ 15 7示例 2:
输入:root = [1] 输出:示例 3:
输入:root = [] 输出:[]约束条件
- 树中节点数目在范围
[0, 2000]内 -1000 <= Node.val <= 1000
二、原题分析
什么是“层序遍历”?
- 定义:按树的层级顺序,从上到下、从左到右访问节点。
- 别名:广度优先遍历(BFS, Breadth-First Search)
- 输出要求:必须按层分组,不能简单返回扁平列表。
为什么这道题重要?
- BFS 的经典应用:是理解队列和层级遍历的基石。
- 面试必考题:几乎所有大厂都会考察层序遍历及其变种。
- 基础模板:后续许多问题(如锯齿形遍历、右视图)都基于此。
- 易错点:如何正确分层是初学者的主要障碍。
💡核心挑战:
普通 BFS 只能保证访问顺序,但无法自动分层!需要巧妙设计才能按层输出。
三、答案构思
面对“层序遍历”问题,关键是如何区分不同层的节点。
✅ 正确思路:广度优先搜索(BFS) + 层级控制
- 核心思想:
- 使用队列存储待访问节点
- 每次处理整层节点:先记录当前队列大小(即当前层节点数),再循环处理
- 实现方式:
- 外层
while控制层数 - 内层
for处理当前层所有节点,并将下一层节点入队
- 外层
❌ 错误思路:普通 BFS(不分层)
// 错误!输出为 [3,9,20,15,7],未分层Queue<TreeNode>queue=newLinkedList<>();queue.offer(root);while(!queue.isEmpty()){TreeNodenode=queue.poll();result.add(node.val);// 扁平列表// ...}我们将实现正确解法,并深入分析其原理。
四、完整答案(Java实现)
方法:广度优先搜索(BFS)
importjava.util.*;classSolution{publicList<List<Integer>>levelOrder(TreeNoderoot){List<List<Integer>>result=newArrayList<>();// 边界条件:空树if(root==null){returnresult;}Queue<TreeNode>queue=newLinkedList<>();queue.offer(root);while(!queue.isEmpty()){// 关键:记录当前层的节点数量intlevelSize=queue.size();List<Integer>currentLevel=newArrayList<>();// 处理当前层的所有节点for(inti=0;i<levelSize;i++){TreeNodenode=queue.poll();currentLevel.add(node.val);// 将下一层节点加入队列if(node.left!=null){queue.offer(node.left);}if(node.right!=null){queue.offer(node.right);}}// 将当前层结果加入最终结果result.add(currentLevel);}returnresult;}}✅使用 ArrayDeque 提升性能:
Queue<TreeNode>queue=newArrayDeque<>();// 比 LinkedList 更高效五、代码分析
核心逻辑详解
层级控制:
int levelSize = queue.size()是最关键的一步!- 在进入内层循环前,队列中恰好包含当前层的所有节点
levelSize锁定了本次要处理的节点数,避免受新入队节点影响
执行流程(以示例1为例):
初始: queue=[3] 第1层: levelSize=1 → poll(3) → add(9,20) → result= 第2层: levelSize=2 → poll(9,20) → add(15,7) → result=[[3],[9,20]] 第3层: levelSize=2 → poll(15,7) → 无新节点 → result=[[3],[9,20],[15,7]]
💡记忆技巧:
“先数人头,再集体行动”
为什么这个方法正确?
通过循环不变式证明:
- 初始化:第1层只有根节点,队列大小=1,正确
- 保持:若第k层处理正确,则其子节点(第k+1层)会按从左到右顺序入队
- 终止:队列空时,所有层已处理完毕
六、时间复杂度与空间复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 每个节点入队出队各一次 |
| 空间复杂度 | O(w) | w 为树的最大宽度,最坏 O(n) |
详细解释:
时间复杂度:O(n)
- 每个节点被访问恰好一次
- 每次操作(入队、出队、添加到列表)均为常数时间
- 故总时间为线性
空间复杂度:O(w)
- 空间消耗主要来自队列存储
- 队列最大长度 = 树的最大宽度
w - 最坏情况(完全二叉树):最后一层有
≈n/2节点 → O(n) - 最好情况(链表):每层1节点 → O(1)
🔍对比 DFS:
若用 DFS 实现层序遍历,需额外存储层级信息,空间复杂度仍为 O(n),且代码更复杂。
七、常见问题解答(FAQ)
Q1:为什么不能在内层循环中直接用queue.size()?
答:因为queue.size()在循环中会动态变化!例如:
// 错误写法for(inti=0;i<queue.size();i++){// size() 不断减小// ...}这会导致只处理部分节点(实际处理size()/2个),因为i增加的同时size()减少。
Q2:能否用 DFS 实现层序遍历?
答:可以,但不推荐。DFS 需要额外参数记录深度:
publicvoiddfs(TreeNodenode,intdepth,List<List<Integer>>result){if(node==null)return;if(depth>=result.size()){result.add(newArrayList<>());}result.get(depth).add(node.val);dfs(node.left,depth+1,result);dfs(node.right,depth+1,result);}- 优点:代码简洁
- 缺点:递归栈空间 O(h),且不符合“广度优先”的直观理解
Q3:如果树非常宽,队列会内存溢出吗?
答:理论上可能,但题目约束n ≤ 2000,完全二叉树最大宽度约1000,内存足够。实际工程中可考虑分批处理。
Q4:如何实现从右到左的层序遍历?
答:只需在添加子节点时先右后左:
if(node.right!=null)queue.offer(node.right);if(node.left!=null)queue.offer(node.left);但注意:这会改变下一层的顺序!若要保持每层从右到左,但整体仍从上到下,需用Collections.reverse(currentLevel)。
Q5:能否不用队列?
答:可以用两个列表交替:
List<TreeNode>current=Arrays.asList(root);while(!current.isEmpty()){List<TreeNode>next=newArrayList<>();// 处理 current// 将子节点加入 nextcurrent=next;}但本质上仍是 BFS,只是手动管理“层”。
八、优化思路
1. 使用 ArrayDeque 替代 LinkedList
ArrayDeque基于循环数组,内存连续,缓存友好LinkedList基于双向链表,对象开销大
Queue<TreeNode>queue=newArrayDeque<>();2. 预分配列表容量
若已知每层节点数,可预分配ArrayList容量:
List<Integer>currentLevel=newArrayList<>(levelSize);减少动态扩容开销。
3. 提前终止(针对变种问题)
- 如求“最小深度”,遇到第一个叶子节点即可返回
- 本题需完整遍历,无法提前终止
4. 并行处理?
理论上可并行处理同一层的节点,但:
- 节点处理无依赖,可并行
- 但入队操作需同步,可能得不偿失
- 通常不值得
5. 流式处理(大数据场景)
若树极大,可设计流式 API,逐层返回结果而不全量存储:
Stream<List<Integer>>levelOrderStream(TreeNoderoot){...}九、数据结构与算法基础知识点回顾
1. BFS vs DFS 对比
| 特性 | BFS | DFS |
|---|---|---|
| 数据结构 | 队列(FIFO) | 栈(LIFO,递归或显式) |
| 空间复杂度 | O(w) | O(h) |
| 适用场景 | 最短路径、层级遍历 | 路径查找、回溯 |
| 层序遍历 | 天然支持 | 需额外深度参数 |
2. 队列的核心作用
- FIFO 保证层级顺序:先入队的节点先处理
- 层级隔离:通过
size()实现层间边界
3. 树的宽度与高度
- 宽度(Width):某层最多节点数
- 高度(Height):根到最远叶子的边数
- 完全二叉树性质:高度 h 的树,宽度最大为 2^h
4. 循环不变式(Loop Invariant)
- 定义:循环前后始终为真的条件
- 作用:证明算法正确性
- 本题不变式:每次外层循环开始时,队列包含当前层所有节点
5. 边界条件处理
- 空树:直接返回空列表
- 单节点:正确处理为
[[val]] - null 子节点:入队前检查,避免 NullPointerException
十、面试官提问环节(模拟对话)
面试官:你用了 BFS,能说说为什么需要
levelSize吗?
你:因为 BFS 队列会动态变化。levelSize锁定了当前层的节点数,确保内层循环只处理这一层,不受新入队节点影响。
面试官:空间复杂度是多少?
你:O(w),w 是树的最大宽度。完全二叉树中 w ≈ n/2,所以最坏 O(n)。
面试官:如果要求从底向上返回层序遍历呢?
你:可以在最后Collections.reverse(result),或者用LinkedList的addFirst方法。
面试官:如何实现锯齿形层序遍历(Zigzag)?
你:用一个布尔变量标记方向,奇数层正常添加,偶数层用add(0, val)或Collections.reverse。
面试官:这道题和“二叉树的右视图”有什么关系?
你:右视图就是每层的最后一个节点。可以在内层循环结束后,取currentLevel.get(currentLevel.size()-1)。
面试官:能否用 DFS 实现?优缺点是什么?
你:可以,DFS 需传递深度参数。优点是代码短;缺点是空间复杂度 O(h),且不符合 BFS 的直观性。
十一、这道算法题在实际开发中的应用
1. 文件系统目录遍历
ls -R或tree命令的底层实现- 按层级展示目录结构,便于用户理解嵌套关系
2. Web 爬虫(广度优先爬取)
- 从种子 URL 开始,逐层爬取链接
- BFS 保证先爬取浅层页面,符合“重要页面优先”原则
3. 社交网络好友推荐
- 从用户出发,逐层扩展好友的好友
- 第1层:直接好友,第2层:好友的好友,依此类推
4. 游戏 AI(关卡探索)
- 迷宫寻路中,BFS 找到最短路径
- 层序遍历用于可视化探索过程
5. 编译器(语法树遍历)
- 按层级分析代码结构
- 顶层:程序,中层:函数,底层:表达式
6. 数据库索引(B+树遍历)
- 虽然 B+树非二叉,但层序遍历思想类似
- 用于调试和可视化索引结构
十二、相关题目推荐
掌握本题后,可挑战以下进阶题目:
| 题号 | 题目 | 关联点 |
|---|---|---|
| 107 | 二叉树的层序遍历 II | 自底向上 |
| 103 | 二叉树的锯齿形层序遍历 | 之字形 |
| 199 | 二叉树的右视图 | 每层最后一个 |
| 637 | 二叉树的层平均值 | 每层统计 |
| 515 | 在每个树行中找最大值 | 每层最值 |
| 116 | 填充每个节点的下一个右侧节点指针 | 层内连接 |
| 117 | 填充每个节点的下一个右侧节点指针 II | 非完美二叉树 |
🔥重点推荐:
- 第103题:在层序遍历基础上增加方向控制,考察细节处理。
- 第199题:层序遍历的典型应用,高频面试题。
十三、总结与延伸
核心收获
BFS 的精髓:
- 队列实现 FIFO
size()技巧实现分层- 循环不变式保证正确性
层级控制的重要性:
- 普通 BFS ≠ 层序遍历
- 必须显式处理层边界
空间复杂度的权衡:
- BFS 空间 ∝ 宽度
- DFS 空间 ∝ 高度
- 根据树形状选择
模板化思维:
- 层序遍历是解决“按层处理”问题的通用模板
- 稍作修改即可应对多种变体
延伸思考
N 叉树的层序遍历?
只需将left/right改为遍历children列表。带权 BFS?
若边有权重,层序遍历不再适用,需用 Dijkstra 算法。分布式层序遍历?
在大规模图(如社交网络)中,需设计分布式 BFS 算法。内存受限场景?
可用迭代加深(IDDFS)模拟 BFS,但时间复杂度升至 O(n²)。
最后建议
- 面试准备:务必手写 BFS 层序遍历,并能解释
levelSize的作用。 - 工程实践:优先使用
ArrayDeque,注意边界条件。 - 算法竞赛:熟练掌握各种层序遍历变体,它们是高频考点。
结语:二叉树的层序遍历是算法学习的里程碑——它简单却深刻,基础却强大。掌握它,你就掌握了 BFS 的灵魂,也为解决更复杂的树和图问题打下了坚实基础。愿你在编程路上,既能写出优雅代码,也能洞察算法之美。
欢迎点赞、收藏、评论交流!你的支持是我持续输出高质量内容的动力!