bigywater.tech
算法与数据结构 · LeetCode 102

LeetCode 102. 二叉树的层序遍历

给定一棵二叉树,按从上到下、每一层从左到右的顺序返回结点值,每一层是结果里的一个子数组。用队列做 BFS 是标准做法,但一个常见陷阱是:内层循环没有先固定"这一层有多少个结点"就直接弹到队列空,这样会把下一层的结点在同一轮里提前处理掉,所有层被合并成一个数组。本文用真实反例验证这个错误,给出正确写法、5 种语言实现,以及按层出队的分步演示。

1 层
陷阱写法在完全二叉树 [1,2,3,4,5,6,7] 上,把本该有的 3 层全部合并成了这 1 层
4 步
分步演示:BFS 按层出队,每一轮先数清楚这一层有几个结点,另附 5 种语言实现

题意与示例

本文只用自己的话概括题意,完整约束以官方页面为准。给定一棵二叉树,按从上到下、每一层从左到右的顺序返回结点值,每一层放在自己的子数组里(不是拍平成一个数组)。一共 5 组用例,覆盖:官方示例、空树、单结点、只有左孩子的链、完全二叉树:

真实实测 · 本机执行输出
[3,9,20,null,null,15,7]  →  [[3], [9, 20], [15, 7]]
[]  →  []
[1]  →  [[1]]
[1,2,null,3]  →  [[1], [2], [3]]
[1,2,3,4,5,6,7]  →  [[1], [2, 3], [4, 5, 6, 7]]

陷阱:内层循环没有固定这一层的结点数,会把所有层合并成一层

标准 BFS 用一个队列做层序遍历没有问题,但一个很容易漏掉的细节是:处理"这一层"的时候,必须先记下队列当前的长度(也就是这一层有多少个结点),再精确弹出这么多个——因为在弹出的过程中,会不断把下一层的孩子结点推进同一个队列。如果偷懒写成"只要队列不空就继续弹",那么下一层的结点会在同一轮循环里被当场处理掉,整棵树的所有层就会被合并成一个层:

真实实测 · 本机执行输出
输入 [3,9,20,null,null,15,7]\n正确写法(先固定 size 再弹出)  →  [[3], [9, 20], [15, 7]]\n陷阱写法(内层用 while q 一直弹到队列空)  →  [[3, 9, 20, 15, 7]]

陷阱写法的内层循环条件也是 while q,和外层一模一样:外层循环第一次进入时,队列里只有根结点 3;但内层循环一旦开始弹出 3、把 9 和 20 推进队列,队列就不再是空的,内层循环会继续把 9、20 也弹出来,再把 15、7 推进去,直到整个队列彻底耗尽——这样只会跑外层循环一次,输出永远只有一个子数组,包含所有结点的 BFS 顺序,完全丢失了"层"的边界。用完全二叉树 [1,2,3,4,5,6,7] 再验证一次:

真实实测 · 本机执行输出
输入 [1,2,3,4,5,6,7]\n正确写法  →  [[1], [2, 3], [4, 5, 6, 7]]\n陷阱写法  →  [[1, 2, 3, 4, 5, 6, 7]]

BFS 按层出队:五种语言的实现

用一个队列做标准 BFS:每一轮先记下 size = 队列长度,正好弹出 size 个结点作为这一层的内容,同时把它们的孩子推入队列供下一轮使用。时间 O(n),空间 O(w)(w 是树的最大宽度)。Python、Java、Go、JavaScript、C++ 都是真实编译运行的,用同一组 5 个测试用例。

Python · 解法代码from collections import deque def level_order(root): if root is None: return [] result = [] q = deque([root]) while q: size = len(q) # 先固定这一层有多少个结点 level = [] for _ in range(size): # 只弹出这么多个,不多不少 node = q.popleft() level.append(node.val) if node.left: q.append(node.left) if node.right: q.append(node.right) result.append(level) return result
真实实测 · Python · 前几组用例的输出
$ python3 solution.py
[3,9,20,null,null,15,7]  →  [[3], [9, 20], [15, 7]]
[]  →  []
[1]  →  [[1]]
[1,2,null,3]  →  [[1], [2], [3]]
[1,2,3,4,5,6,7]  →  [[1], [2, 3], [4, 5, 6, 7]]
Java · 解法代码class Solution { public List<List<Integer>> levelOrder(TreeNode root) { List<List<Integer>> result = new ArrayList<>(); if (root == null) return result; Deque<TreeNode> q = new ArrayDeque<>(); q.add(root); while (!q.isEmpty()) { int size = q.size(); // 先固定这一层有多少个结点 List<Integer> level = new ArrayList<>(); for (int i = 0; i < size; i++) { // 只弹出这么多个,不多不少 TreeNode node = q.poll(); level.add(node.val); if (node.left != null) q.add(node.left); if (node.right != null) q.add(node.right); } result.add(level); } return result; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java && java -cp build Main
[3,9,20,null,null,15,7]  →  [[3], [9, 20], [15, 7]]
[]  →  []
[1]  →  [[1]]
[1,2,null,3]  →  [[1], [2], [3]]
[1,2,3,4,5,6,7]  →  [[1], [2, 3], [4, 5, 6, 7]]
Go · 解法代码func levelOrder(root *TreeNode) [][]int { result := [][]int{} if root == nil { return result } q := []*TreeNode{root} for len(q) > 0 { size := len(q) // 先固定这一层有多少个结点 level := []int{} for i := 0; i < size; i++ { // 只弹出这么多个,不多不少 node := q[0] q = q[1:] level = append(level, node.Val) if node.Left != nil { q = append(q, node.Left) } if node.Right != nil { q = append(q, node.Right) } } result = append(result, level) } return result }
真实实测 · Go · 前几组用例的输出
$ go run solution.go
[3,9,20,null,null,15,7]  →  [[3], [9, 20], [15, 7]]
[]  →  []
[1]  →  [[1]]
[1,2,null,3]  →  [[1], [2], [3]]
[1,2,3,4,5,6,7]  →  [[1], [2, 3], [4, 5, 6, 7]]
JavaScript · 解法代码function levelOrder(root) { const result = []; if (root === null) return result; let q = [root]; while (q.length) { const size = q.length; // 先固定这一层有多少个结点 const level = []; for (let i = 0; i < size; i++) { // 只弹出这么多个,不多不少 const node = q.shift(); level.push(node.val); if (node.left) q.push(node.left); if (node.right) q.push(node.right); } result.push(level); } return result; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js
[3,9,20,null,null,15,7]  →  [[3], [9, 20], [15, 7]]
[]  →  []
[1]  →  [[1]]
[1,2,null,3]  →  [[1], [2], [3]]
[1,2,3,4,5,6,7]  →  [[1], [2, 3], [4, 5, 6, 7]]
C++ · 解法代码class Solution { public: vector<vector<int>> levelOrder(TreeNode* root) { vector<vector<int>> result; if (!root) return result; queue<TreeNode*> q; q.push(root); while (!q.empty()) { int size = q.size(); // 先固定这一层有多少个结点 vector<int> level; for (int i = 0; i < size; i++) { // 只弹出这么多个,不多不少 TreeNode* node = q.front(); q.pop(); level.push_back(node->val); if (node->left) q.push(node->left); if (node->right) q.push(node->right); } result.push_back(level); } return result; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin
[3,9,20,null,null,15,7]  →  [[3], [9, 20], [15, 7]]
[]  →  []
[1]  →  [[1]]
[1,2,null,3]  →  [[1], [2], [3]]
[1,2,3,4,5,6,7]  →  [[1], [2, 3], [4, 5, 6, 7]]
真实实测 · 本机执行输出
共 5 组用例(含边界),以 Python 结果为基准逐组比对:\n  Python      通过 5/5\n  Java        通过 5/5\n  Go          通过 5/5\n  JavaScript  通过 5/5\n  C++         通过 5/5

随机对拍

Python 版本的 BFS 实现另外和一个独立写的 DFS 版本做了随机对拍:DFS 版用递归,把结点按访问到的深度放进对应的子数组(第一次到达某个深度就新建一个子数组)。

真实实测 · 本机执行输出
随机对拍 30,000 组(结点数 0 到 10 的随机二叉树,值取 0-4),BFS 版与 DFS 版的结果全部一致,0 处不一致。

另外四种语言只在上面 5 组固定用例上验证过,没有做大规模随机对拍。

分步演示:每一轮先数清楚这一层有几个结点

下面的演示用官方示例 [3,9,20,null,null,15,7]:每一轮先在队列里数清楚这一层有几个结点(size),只弹出这么多个,加进结果数组的新一行,再把它们的孩子推入队列供下一轮使用。蓝色是本层正在处理的结点,绿色是已经处理完、值已经写进结果的结点。每一帧都是把上面的解法在这组输入上真实执行记录下来的。

层序遍历:每一轮先数清楚这一层有几个结点
本层正在处理的结点已经处理完的结点

    参考与说明

    • 官方题目页:LeetCode 102. 二叉树的层序遍历。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。
    • 没有覆盖:不用队列、用两个数组交替表示"当前层"和"下一层"的写法。
    京ICP备2026067502号-1京公网安备11010502063487号