当前:LC102 · 二叉树的层序遍历 · 首次出现于 Day 17 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC102 · Binary Tree Level Order Traversal · 树 / BFS

二叉树的层序遍历:推进容易,分层才难

BFS 队列天然按层推进,但直接 while 到空会把两层混在一起。真正的技巧是每轮只处理「本层长度」个节点。

用队列做广度优先遍历。每轮循环开头记录 size = len(queue),这是当前层的节点数;连续出队 size 次,收集成一层的数组,出队的同时把左右孩子入队(它们属于下一层)。如此循环直到队列空。时间 O(n),空间 O(n)。

时间 O(n)空间 O(n)结论先行 · 全文约 6 节
导读

给你二叉树的根节点,返回按层序遍历(从上到下、从左到右)的结果,每层一个数组。

示例:root = [3,9,20,null,null,15,7] → [[3],[9,20],[15,7]]。

看起来只要把 BFS 的「出队 + 入队孩子」跑一遍就行——但注意,BFS 只是按层推进,它本身不告诉你「这一层在哪结束」。分层的答案藏在 size 这个变量里。

这篇文章从「看似理所当然的 BFS」讲起,用一个陷阱让你亲眼看到分层失败的后果,再引出 size 切层这个不起眼却决定性的一行代码。

BFS 只负责推进,不负责分层

层序遍历 = 广度优先遍历(BFS),用队列实现:根先入队,每轮出队一个节点、访问它、把左右孩子入队。

队列的 FIFO 特性保证:第 k 层的所有节点,全部在第 k+1 层的节点之前被访问。这是 BFS 的「按层推进」性质。

但请注意措辞:BFS 保证的是「第 k 层先于第 k+1 层」,它并没有保证「我能知道某一层里到底有哪几个节点」。这正是下面要面对的陷阱。

FIFO队列先进先出:同层先入先出,上层整体先于下层。这是 BFS 唯一保证的事。
BFS 队列推进:3 → 9,20 → 15,7 按层入队
3920157
队列3
根 3 入队

陷阱:直接 while 到空,两层全混

一个看起来很自然的写法:while 队列非空,每次出队一个节点,把它放进「当前层」数组,入队它的孩子。问题来了——这个「当前层」数组什么时候该提交到结果里?

如果只在队列为空时提交,那么所有节点都会进同一个数组:[[3, 9, 20, 15, 7]]。这不是层序遍历,这是把整棵树拍平了。

再看更深层的错误:如果把「出队一次就提交一层」也写错(内层循环跑了整个队列),队列会不断增长,最终同层节点被拆进不同层、跨层节点被并进同一层——彻底乱套。

根源只有一个:BFS 队列里同时装着「当前层」和「下一层」,而代码不知道它们在队列里的分界线在哪。

陷阱BFS 只保证「上层先访问」,不保证「我知道这一层有几个」。分界线必须由我们自己记录。
错误写法:while 队列非空,把出队节点全塞进同一个 level
3920157
队列3920157
结果
[3, 9, 20]← 层被拍平
错误实现把 3、9、20 全放进一个 level

size 切层:记录本层人数,只处理这么多

解法是让「一层的处理」成为循环的一个完整单元:每轮循环开头,记下 size = 当前队列长度,这就是本层还剩下的节点数。

然后连续出队恰好 size 次:这 size 个节点构成一层,收集进 level;出队过程中入队的孩子,全都属于下一层,不会混进当前层。

为什么安全?因为 size 是在「本层一个都还没出」的时刻拍的快照。此后无论队列怎么变长(下一层的孩子不断入队),我们只出队 size 个——这些恰好是本层的全部。

一句话:先冻结本层人数,再动手出队。这样 BFS 的「按层推进」才被补上了「按层切分」。

size 快照size 是轮次开头的队列长度。它是「本层人数」的冻结值,防止下一层的孩子混进来。
size 切层:每轮只出队 size 个,孩子留给下一轮
3920157
队列3size = 1(前 1 个是本层)
size = 1已出队 0当前层 [3]
第 1 轮:size=1,出队 3 → 层 [3]

完整跑一遍:size 如何保证分层

用 root = [3, 9, 20, null, null, 15, 7] 逐轮核对。

第 1 轮:队列 [3],size = 1。出队 3(入队 9、20),level = [3]。提交 → result = [[3]]。此时队列 [9, 20]。

第 2 轮:队列 [9, 20],size = 2。出队 9(无孩子),level = [9];出队 20(入队 15、7),level = [9, 20]。提交 → result = [[3], [9, 20]]。此时队列 [15, 7]。

第 3 轮:队列 [15, 7],size = 2。出队 15、7(都无孩子),level = [15, 7]。提交 → result = [[3], [9, 20], [15, 7]]。队列空,结束。

注意第 2 轮的关键:size 是在出队 9 之前冻结的 2。就算出队 9 时入队了 15、7,内层循环也只跑 2 次——15、7 被留到第 3 轮。这就是「冻结」的意义。

逐轮核对 size、level、result
3920157
队列3size = 1(前 1 个是本层)
size = 1已出队 1当前层 [3]
第 1 轮结束:result = [[3]]

易错点与变式

最容易写错的不是 size 本身,而是把「size 快照」写在出队之后——那样 size 已经包含了下一层的孩子,本层会少算。快照必须写在每轮循环的第一行。

另一个常见错误:忘了处理空树。root == nil 时应该返回空数组而不是 panic。

变式一:LC107 自底向上,把 result 反转即可。

变式二:LC199 右视图,取每层最后一个节点。它甚至不用维护完整 level,只要记住每层最后一个出队的值。

变式三:LC637 每层平均值、LC429 N 叉树层序——都是同一种 size 切层模板,只是收集的数据不同。

模板size 切层是层序遍历的通用骨架:LC107/LC199/LC637/LC429 都只是换一层里收集什么。

复杂度

时间 O(n):每个节点恰好入队一次、出队一次。

空间 O(n):最坏情况(最后一层满节点)队列长度约 n/2;结果数组本身也占 O(n)。

相比递归的深度优先,BFS 的空间是队列主导;DFS 的空间是调用栈主导(树高)。这道题要求「按层输出」,DFS 也可以做(记录每层的 depth 分组),但队列版是直觉与实现最直接的选择。

Go:队列 + size 切层

solution.goGo
func levelOrder(root *TreeNode) [][]int {
if root == nil { return nil }
res, queue := [][]int{}, []*TreeNode{root}
for len(queue) > 0 {
size := len(queue)
level := make([]int, 0, size)
for i := 0; i < size; i++ {
n := queue[0]
queue = queue[1:]
level = append(level, n.Val)
if n.Left != nil { queue = append(queue, n.Left) }
if n.Right != nil { queue = append(queue, n.Right) }
}
res = append(res, level)
}
return res
}

1空树返回 nil。

2队列初始为根。

3记录本层节点数——必须写在出队之前。

4本层数组预留容量。

5恰好出队 size 个。

6孩子节点入队,属于下一层。

7每轮结束提交一层。

总结

层序 = BFS 队列 + 每轮 size 快照,先冻结本层人数再出队。

  • BFS 只保证按层推进,不保证你知道这一层有几个——分界必须自己记。
  • size = 轮次开头的队列长度,是「本层人数」的冻结值。
  • 先冻结、再出队:下一层的孩子永远不会混进当前层。
  • LC107 反转、LC199 取末位、LC637 求均值都是同款模板。
同族题目
LC107二叉树的层序遍历 IILC199二叉树的右视图LC104二叉树的最大深度