潮州市网站建设_网站建设公司_模板建站_seo优化
2026/1/17 15:32:57 网站建设 项目流程

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)
  • 输出要求:必须按层分组,不能简单返回扁平列表。

为什么这道题重要?

  1. BFS 的经典应用:是理解队列和层级遍历的基石。
  2. 面试必考题:几乎所有大厂都会考察层序遍历及其变种。
  3. 基础模板:后续许多问题(如锯齿形遍历、右视图)都基于此。
  4. 易错点:如何正确分层是初学者的主要障碍。

💡核心挑战
普通 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 对比

特性BFSDFS
数据结构队列(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),或者用LinkedListaddFirst方法。

面试官:如何实现锯齿形层序遍历(Zigzag)?
:用一个布尔变量标记方向,奇数层正常添加,偶数层用add(0, val)Collections.reverse

面试官:这道题和“二叉树的右视图”有什么关系?
:右视图就是每层的最后一个节点。可以在内层循环结束后,取currentLevel.get(currentLevel.size()-1)

面试官:能否用 DFS 实现?优缺点是什么?
:可以,DFS 需传递深度参数。优点是代码短;缺点是空间复杂度 O(h),且不符合 BFS 的直观性。


十一、这道算法题在实际开发中的应用

1. 文件系统目录遍历

  • ls -Rtree命令的底层实现
  • 按层级展示目录结构,便于用户理解嵌套关系

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题:层序遍历的典型应用,高频面试题。

十三、总结与延伸

核心收获

  1. BFS 的精髓

    • 队列实现 FIFO
    • size()技巧实现分层
    • 循环不变式保证正确性
  2. 层级控制的重要性

    • 普通 BFS ≠ 层序遍历
    • 必须显式处理层边界
  3. 空间复杂度的权衡

    • BFS 空间 ∝ 宽度
    • DFS 空间 ∝ 高度
    • 根据树形状选择
  4. 模板化思维

    • 层序遍历是解决“按层处理”问题的通用模板
    • 稍作修改即可应对多种变体

延伸思考

  • N 叉树的层序遍历
    只需将left/right改为遍历children列表。

  • 带权 BFS
    若边有权重,层序遍历不再适用,需用 Dijkstra 算法。

  • 分布式层序遍历
    在大规模图(如社交网络)中,需设计分布式 BFS 算法。

  • 内存受限场景
    可用迭代加深(IDDFS)模拟 BFS,但时间复杂度升至 O(n²)。

最后建议

  • 面试准备:务必手写 BFS 层序遍历,并能解释levelSize的作用。
  • 工程实践:优先使用ArrayDeque,注意边界条件。
  • 算法竞赛:熟练掌握各种层序遍历变体,它们是高频考点。

结语:二叉树的层序遍历是算法学习的里程碑——它简单却深刻,基础却强大。掌握它,你就掌握了 BFS 的灵魂,也为解决更复杂的树和图问题打下了坚实基础。愿你在编程路上,既能写出优雅代码,也能洞察算法之美。

欢迎点赞、收藏、评论交流!你的支持是我持续输出高质量内容的动力!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询