二叉树的层序遍历
队列负责让节点从上到下出场;每轮开始时冻结的 levelSize,负责把同一层牢牢分在一起。
按从上到下、从左到右的顺序,把二叉树每一层分别输出。
既保证 BFS 顺序,又保留层与层之间的边界。
每轮先冻结队列长度,只处理这批节点;新加入的孩子留给下一轮。
先说结论:这道题到底解决什么
普通 BFS 只能给出访问顺序,怎样让队列明确知道一层在哪里结束,并保证下一层新入队的节点不会混进当前层?
中心结论:每轮先冻结队列长度,只处理这批节点;新加入的孩子留给下一轮。
- 1.为什么每轮开始时的 queue 恰好装着当前层全部节点?
- 2.为什么必须先冻结 levelSize,而不能让内层循环一直处理到队列为空?
- 3.左右孩子的入队顺序如何决定同一层的输出顺序?
完整题目与题意拆解
给定二叉树根节点 root,返回节点值的层序遍历结果,也就是逐层从左到右访问所有节点。
返回值是二维数组。外层代表层,内层保存这一层的节点值。
- • 空树返回 [],不是 [[]]。
- • 同一层保持从左到右。
- • 下一层节点由当前层节点的孩子产生。
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]普通 BFS 能得到 [3,9,20,15,7],但题目还要求知道哪些节点属于同一层。
队列中始终按发现顺序保存待处理节点。每轮开始时,队列中已有的节点恰好组成当前层。
输出为什么需要明确的层边界
先展示整棵树和期望的二维结果,再让不同层以横向色带分开,明确算法必须回答的不只是访问顺序。
第一层方案:暴力做法
可以先做一次普通 BFS 得到一维顺序,再为每个节点额外记录 depth,最后按 depth 分组。
这个方案仍需维护节点与深度的配对。更直接的做法是在每轮开始时利用队列现状识别整层。
queue 保存 (node, depth)
依次弹出后放入 result[depth]
额外状态:每个节点都携带 depth队列一直处理到空会发生什么
演示根节点出队后孩子立即加入,如果内层条件跟随 queue 长度变化,下一层就会被错误吞进当前层。
优化方向:如果目标就是从上到下逐层输出,BFS 的队列顺序与题意完全一致,状态也更直观。
整体地图:先做什么,再做什么
层序遍历的难点不是把节点放进队列,而是划清层与层的边界。我们先观察轮开始时的队列快照,再把这一刻的长度冻结成 levelSize。
本轮只消费 levelSize 个旧节点;处理过程中加入队尾的孩子属于下一层,必须留到下一轮。这样队列既负责从上到下,冻结长度又负责按层分组。
- • 队列维护访问先后顺序。
- • levelSize 固定本轮消费数量。
- • level 切片只收集本轮节点值。
把队列在轮开始时拍成一张当前层快照
根节点先入队。每轮外层循环开始时,队列中保存的恰好是尚未处理的最浅一层节点。令 levelSize=len(queue),就相当于给这批节点加上一条不可移动的层边界。
处理一个节点时,它的孩子会进入队尾。因为本轮只执行 levelSize 次,这些新节点不会被提前消费;本轮结束时,队列便自然变成下一层的完整快照。
处理根 3 时,冻结长度是 1。即使 9 和 20 随后入队,本轮也只弹出 3;下一轮才冻结长度 2。冻结快照给当前层画一道边界
轮开始时用括号锁住队列前 levelSize 个元素,后续新入队节点显示在边界之外等待。
固定消费旧节点,让新孩子在队尾等待下一轮
每轮创建容量为 levelSize 的 level。内层循环固定执行 levelSize 次:弹出队首、记录节点值,再按左后右把非空孩子加入队尾。
内层结束后把 level 追加到 result。此刻上一层已完全出队,下一层已按从左到右的顺序排在队列中,外层循环可以重复同一过程。
- • 弹出使用 FIFO,不能替换成栈。
- • 左孩子先入队,保证同层从左到右。
- • 空树直接返回空结果,而不是包含一个空层。
- • 队列可以增长,但本轮计数不能跟着增长。
空树直接返回空结果。根节点先入队;只要队列不空,就创建当前层数组并冻结队列长度。
循环固定次数,每次弹出队首,把值加入 level,再按左、右顺序把非空孩子加入队尾。最后把 level 加入答案。
消费旧节点,排好下一层
节点从队首逐个离开,左右孩子从队尾进入;当前层结果与下一层队列在画面中保持分区。
为什么每一轮都恰好输出一整层
用层数归纳。开始时队列只有根节点,因此第一轮快照正好是第 0 层。假设某轮开始时队列按从左到右保存第 k 层全部节点。
固定消费 levelSize 次会恰好移除第 k 层。每个节点按左后右加入其孩子,所以所有第 k+1 层节点按父节点从左到右、同父节点左到右的顺序进入队尾。
本轮不消费新孩子,因此下一轮开始时队列恰好是第 k+1 层。归纳成立,result 中每个切片都准确对应一层。
- • 轮开始时队列恰好包含当前层全部节点,顺序为从左到右。
- • 固定消费 levelSize 次,确保本轮不会处理新加入的下一层孩子。
- • 孩子按左后右入队,所以归纳得到下一轮仍保持从左到右。
完整执行过程
重点观察队列上方的冻结括号:青色槽位属于本轮,后来进入的琥珀色槽位只能等待下一轮。
- 1把根节点 3 入队,第一轮冻结 levelSize=1。
- 2弹出 3 写入当前 level,并把 9、20 依次加入队尾。
- 3本轮已经消费 1 个旧节点,提交 [3],不继续处理新入队节点。
- 4第二轮冻结 levelSize=2,依次处理 9 和 20,并让 15、7 留在队尾。
- 5重复直到队列为空,得到 [[3],[9,20],[15,7]]。
完整跑出三层二维结果
依次冻结 1、2、2 个节点,展示每轮提交的 level,并同步累积最终二维结果。
把动画和 Go 代码逐行对应
每一帧使用稳定的语义行 ID,不依赖容易漂移的数字行号。先观察分支为什么成立, 再看对应代码,而不是先背模板。
代码中的两个循环分别控制什么
高亮外层的“还有层”、冻结的 levelSize 和固定次数内层循环,让每行代码对应一个队列动作。
题目不仅要访问顺序,还要保留每一层的边界。
BFS 必须先处理最早发现的节点。
本轮只处理开始时已有的 1 个节点,新孩子属于下一层。
出队的节点属于冻结范围,因此属于当前层。
左孩子先入队,保证下一层从左到右;冻结次数仍是 1,不会继续消费它们。
固定次数已经执行完,本轮边界清晰结束。
本轮冻结两次,所以两位同层节点会一起提交。
levelSize 早已固定为 2,enqueue 不会改变本轮循环次数。
每个节点都恰好入队一次、出队一次。
队列不空并不代表当前层还没结束,它可能只装着下一层。
完整 Go 提交代码与最小测试
1func levelOrder(root *TreeNode) [][]int {2 if root == nil { return [][]int{} }3 4 result := make([][]int, 0)5 queue := []*TreeNode{root}6 7 for len(queue) > 0 {8 levelSize := len(queue)9 level := make([]int, 0, levelSize)10 for i := 0; i < levelSize; i++ {11 node := queue[0]; queue = queue[1:]12 level = append(level, node.Val)13 if node.Left != nil { queue = append(queue, node.Left) }14 if node.Right != nil { queue = append(queue, node.Right) }15 }16 result = append(result, level)17 }18 return result19}// 主例:三层且最后一层来自同一父节点
fmt.Println(levelOrder(buildTree([]any{3, 9, 20, nil, nil, 15, 7}))) // [[3] [9 20] [15 7]]
// 空树不能返回一个空层
fmt.Println(levelOrder(nil)) // []
// 单节点
fmt.Println(levelOrder(&TreeNode{Val: 1})) // [[1]]
// 链状树仍需逐层分组
fmt.Println(levelOrder(buildTree([]any{1, 2, nil, 3}))) // [[1] [2] [3]]正确性与复杂度
每个节点只入队一次、出队一次,并被写入结果一次。
队列最多同时保存树的最大层宽 w 个节点;若计入返回结果则总输出空间为 O(n)。
最容易写错的地方
内层一直循环到 queue 为空,导致下一层混进当前层。
用栈替代队列,遍历退化成 DFS。
右孩子先入队,破坏同层从左到右顺序。
空树返回 [[]] 而不是 []。
最后复盘:带走逻辑链
- 1.queue 决定从上到下。
- 2.levelSize 决定当前层边界。
- 3.左、右孩子的入队顺序决定同层顺序。
- 4.同一模型可迁移到 LC107、LC199 和多源 BFS。
- 1.用队列做 BFS,根节点先入队。
- 2.每轮开始保存 levelSize=len(queue),它就是当前层节点数。
- 3.固定弹出 levelSize 个节点,记录值并把左右孩子加入队尾。
- 4.每轮结束追加 level;时间 O(n),额外队列空间 O(w)。