二叉树的层序遍历:推进容易,分层才难
BFS 队列天然按层推进,但直接 while 到空会把两层混在一起。真正的技巧是每轮只处理「本层长度」个节点。
用队列做广度优先遍历。每轮循环开头记录 size = len(queue),这是当前层的节点数;连续出队 size 次,收集成一层的数组,出队的同时把左右孩子入队(它们属于下一层)。如此循环直到队列空。时间 O(n),空间 O(n)。
给你二叉树的根节点,返回按层序遍历(从上到下、从左到右)的结果,每层一个数组。
示例: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 唯一保证的事。
陷阱:直接 while 到空,两层全混
一个看起来很自然的写法:while 队列非空,每次出队一个节点,把它放进「当前层」数组,入队它的孩子。问题来了——这个「当前层」数组什么时候该提交到结果里?
如果只在队列为空时提交,那么所有节点都会进同一个数组:[[3, 9, 20, 15, 7]]。这不是层序遍历,这是把整棵树拍平了。
再看更深层的错误:如果把「出队一次就提交一层」也写错(内层循环跑了整个队列),队列会不断增长,最终同层节点被拆进不同层、跨层节点被并进同一层——彻底乱套。
根源只有一个:BFS 队列里同时装着「当前层」和「下一层」,而代码不知道它们在队列里的分界线在哪。
陷阱BFS 只保证「上层先访问」,不保证「我知道这一层有几个」。分界线必须由我们自己记录。
size 切层:记录本层人数,只处理这么多
解法是让「一层的处理」成为循环的一个完整单元:每轮循环开头,记下 size = 当前队列长度,这就是本层还剩下的节点数。
然后连续出队恰好 size 次:这 size 个节点构成一层,收集进 level;出队过程中入队的孩子,全都属于下一层,不会混进当前层。
为什么安全?因为 size 是在「本层一个都还没出」的时刻拍的快照。此后无论队列怎么变长(下一层的孩子不断入队),我们只出队 size 个——这些恰好是本层的全部。
一句话:先冻结本层人数,再动手出队。这样 BFS 的「按层推进」才被补上了「按层切分」。
size 快照size 是轮次开头的队列长度。它是「本层人数」的冻结值,防止下一层的孩子混进来。
完整跑一遍: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 本身,而是把「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 切层
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 求均值都是同款模板。