概述
二叉树的层序遍历(Level Order Traversal)是一种按层级从上到下、从左到右访问二叉树节点的遍历方式。它是广度优先搜索(BFS)在二叉树上的典型应用,是数据结构和算法面试中的高频考点。
特点
- 遍历顺序:按层级顺序访问节点
- 核心数据结构:队列(Queue)
- 算法思想:广度优先搜索(BFS)
- 时间复杂度:O(n)
- 空间复杂度:O(w),w为二叉树的最大宽度
应用场景
- 打印二叉树的层级结构
- 寻找二叉树的最短路径
- 序列化和反序列化二叉树
- 计算二叉树的宽度和深度
基础概念
二叉树节点定义
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) {
this.val = val;
}
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
广度优先搜索(BFS)原理
BFS是一种图遍历算法,其核心思想是:
- 从起始节点开始
- 先访问所有距离为1的节点
- 再访问所有距离为2的节点
- 依此类推,直到访问完所有节点
队列数据结构
队列是实现BFS的关键数据结构:
- FIFO:先进先出(First In First Out)
- 主要操作:offer()入队,poll()出队
- Java实现:LinkedList、ArrayDeque
标准实现:队列迭代法
算法思路
- 将根节点加入队列
- 当队列不为空时:
- 取出队首节点
- 访问该节点
- 将该节点的左右子节点(如果存在)加入队列
- 重复步骤2直到队列为空
代码实现
import java.util.*;
public class BinaryTreeLevelOrder {
/**
* 二叉树层序遍历 - 标准实现
* @param root 二叉树根节点
* @return 层序遍历结果列表
*/
public List<Integer> levelOrder(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
result.add(node.val);
// 先左后右加入队列
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
return result;
}
}
执行过程示例
以下面的二叉树为例:
3
/ \
9 20
/ \
15 7
执行过程:
- 初始:queue = [3], result = []
- 处理3:queue = [9, 20], result = [3]
- 处理9:queue = [20], result = [3, 9]
- 处理20:queue = [15, 7], result = [3, 9, 20]
- 处理15:queue = [7], result = [3, 9, 20, 15]
- 处理7:queue = [], result = [3, 9, 20, 15, 7]
分层输出实现
问题描述
有时我们需要按层级分组输出结果,即每一层的节点值作为一个子列表。
方法一:使用队列大小控制
public List<List<Integer>> levelOrderGrouped(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size(); // 当前层的节点数量
List<Integer> currentLevel = new ArrayList<>();
// 处理当前层的所有节点
for (int i = 0; i < levelSize; i++) {
TreeNode node = 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);
}
return result;
}
方法二:使用null分隔符
public List<List<Integer>> levelOrderWithNull(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
queue.offer(null); // null作为层分隔符
List<Integer> currentLevel = new ArrayList<>();
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
if (node == null) {
// 遇到分隔符,当前层结束
result.add(new ArrayList<>(currentLevel));
currentLevel.clear();
// 如果队列不为空,添加下一层的分隔符
if (!queue.isEmpty()) {
queue.offer(null);
}
} else {
currentLevel.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
return result;
}
递归实现方法
算法思路
递归实现层序遍历的核心是维护层级信息:
- 使用深度优先搜索(DFS)
- 传递当前节点的层级信息
- 根据层级将节点值添加到对应的结果列表中
代码实现
public List<List<Integer>> levelOrderRecursive(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}
dfs(root, 0, result);
return result;
}
private void dfs(TreeNode node, int level, List<List<Integer>> result) {
if (node == null) {
return;
}
// 如果当前层级的列表不存在,创建新列表
if (level >= result.size()) {
result.add(new ArrayList<>());
}
// 将当前节点值添加到对应层级
result.get(level).add(node.val);
// 递归处理左右子树
dfs(node.left, level + 1, result);
dfs(node.right, level + 1, result);
}
递归vs迭代对比
| 特性 | 递归实现 | 迭代实现 |
|---|---|---|
| 代码简洁性 | 更简洁 | 相对复杂 |
| 空间复杂度 | O(h) 递归栈 | O(w) 队列空间 |
| 理解难度 | 需要理解递归 | 更直观 |
| 执行效率 | 函数调用开销 | 更高效 |
| 栈溢出风险 | 深度过大时有风险 | 无风险 |
变种问题解析
1. 二叉树的右视图(LeetCode 199)
问题:返回从右侧观察二叉树时能看到的节点值。
public List<Integer> rightSideView(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
// 每层的最后一个节点就是右视图能看到的
if (i == levelSize - 1) {
result.add(node.val);
}
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
return result;
}
2. 二叉树的锯齿形层序遍历(LeetCode 103)
问题:奇数层从左到右,偶数层从右到左。
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
boolean leftToRight = true;
while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> currentLevel = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
if (leftToRight) {
currentLevel.add(node.val);
} else {
currentLevel.add(0, node.val); // 头部插入实现逆序
}
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
result.add(currentLevel);
leftToRight = !leftToRight; // 切换方向
}
return result;
}
3. 自底向上的层序遍历(LeetCode 107)
问题:从叶子节点所在层到根节点所在层,逐层从左到右遍历。
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> currentLevel = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
currentLevel.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
result.add(0, currentLevel); // 头部插入实现逆序
}
return result;
}
4. 在每个树行中找最大值(LeetCode 515)
public List<Integer> largestValues(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
int maxVal = Integer.MIN_VALUE;
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
maxVal = Math.max(maxVal, node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
result.add(maxVal);
}
return result;
}
复杂度分析与优化
时间复杂度分析
-
标准层序遍历:O(n)
- 每个节点被访问一次
- 队列操作为O(1)
- 总时间复杂度为O(n)
-
分层输出:O(n)
- 虽然有嵌套循环,但内层循环总次数等于节点数
- 时间复杂度仍为O(n)
空间复杂度分析
-
队列空间:O(w)
- w为二叉树的最大宽度
- 最坏情况下,完全二叉树的最大宽度为n/2
- 因此空间复杂度为O(n)
-
递归实现:O(h)
- h为二叉树的高度
- 最坏情况下(斜树),h = n
- 最好情况下(平衡树),h = log n
优化策略
1. 队列选择优化
// 使用ArrayDeque替代LinkedList,性能更好
Queue<TreeNode> queue = new ArrayDeque<>();
2. 内存预分配
// 预估结果大小,减少动态扩容
List<Integer> result = new ArrayList<>(estimatedSize);
3. 双端队列实现锯齿形遍历
public List<List<Integer>> zigzagLevelOrderOptimized(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}
Deque<TreeNode> deque = new ArrayDeque<>();
deque.offer(root);
boolean leftToRight = true;
while (!deque.isEmpty()) {
int levelSize = deque.size();
List<Integer> currentLevel = new ArrayList<>();
if (leftToRight) {
// 从左到右:从前面取,向后面放
for (int i = 0; i < levelSize; i++) {
TreeNode node = deque.pollFirst();
currentLevel.add(node.val);
if (node.left != null) {
deque.offerLast(node.left);
}
if (node.right != null) {
deque.offerLast(node.right);
}
}
} else {
// 从右到左:从后面取,向前面放
for (int i = 0; i < levelSize; i++) {
TreeNode node = deque.pollLast();
currentLevel.add(node.val);
if (node.right != null) {
deque.offerFirst(node.right);
}
if (node.left != null) {
deque.offerFirst(node.left);
}
}
}
result.add(currentLevel);
leftToRight = !leftToRight;
}
return result;
}
相关LeetCode题目
基础题目
| 题号 | 题目 | 难度 | 核心考点 |
|---|---|---|---|
| 102 | 二叉树的层序遍历 | 中等 | 基础BFS |
| 107 | 二叉树的层序遍历II | 中等 | 逆序输出 |
| 103 | 二叉树的锯齿形层序遍历 | 中等 | 方向控制 |
| 199 | 二叉树的右视图 | 中等 | 层级最后元素 |
| 515 | 在每个树行中找最大值 | 中等 | 层级统计 |
| 637 | 二叉树的层平均值 | 简单 | 层级计算 |
| 429 | N叉树的层序遍历 | 中等 | 多叉树扩展 |
进阶题目
| 题号 | 题目 | 难度 | 核心考点 |
|---|---|---|---|
| 116 | 填充每个节点的下一个右侧节点指针 | 中等 | 层级连接 |
| 117 | 填充每个节点的下一个右侧节点指针II | 中等 | 非完全二叉树 |
| 314 | 二叉树的垂直遍历 | 中等 | 坐标系遍历 |
| 987 | 二叉树的垂直序遍历 | 困难 | 复杂排序 |
题目解析示例
LeetCode 637: 二叉树的层平均值
public List<Double> averageOfLevels(TreeNode root) {
List<Double> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
long sum = 0; // 使用long避免溢出
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
sum += node.val;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
result.add((double) sum / levelSize);
}
return result;
}
面试问题解析
常见面试问题
1. BFS和DFS的区别是什么?
回答要点:
- 遍历顺序:BFS按层级,DFS按深度
- 数据结构:BFS用队列,DFS用栈(或递归)
- 空间复杂度:BFS为O(w),DFS为O(h)
- 应用场景:BFS适合最短路径,DFS适合路径搜索
2. 为什么层序遍历要用队列?
回答要点:
- 队列的FIFO特性符合层序遍历的需求
- 先访问的节点,其子节点也应该先被访问
- 保证了同一层节点的访问顺序
3. 如何优化层序遍历的空间复杂度?
回答要点:
- 使用两个变量记录当前层和下一层的节点数
- 使用双端队列优化锯齿形遍历
- 对于特定问题,可以使用递归减少队列空间
4. 层序遍历如何处理空节点?
回答要点:
- 标准实现中不将null加入队列
- 特殊需求下可以使用null作为层分隔符
- 序列化时需要考虑null节点的表示
编程技巧
1. 边界条件处理
// 检查根节点
if (root == null) {
return new ArrayList<>();
}
// 检查子节点
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
2. 层级信息维护
// 方法1:记录队列大小
int levelSize = queue.size();
for (int i = 0; i < levelSize; i++) {
// 处理当前层节点
}
// 方法2:使用Pair记录层级
class Pair {
TreeNode node;
int level;
Pair(TreeNode node, int level) {
this.node = node;
this.level = level;
}
}
3. 结果收集优化
// 预分配空间
List<List<Integer>> result = new ArrayList<>();
// 动态添加层级
if (level >= result.size()) {
result.add(new ArrayList<>());
}
result.get(level).add(node.val);
实际应用场景
1. 文件系统遍历
public class FileSystemTraversal {
public void traverseDirectory(File directory) {
if (!directory.isDirectory()) {
return;
}
Queue<File> queue = new LinkedList<>();
queue.offer(directory);
while (!queue.isEmpty()) {
int levelSize = queue.size();
System.out.println("=== Level " + getCurrentLevel() + " ===");
for (int i = 0; i < levelSize; i++) {
File current = queue.poll();
System.out.println(current.getName());
if (current.isDirectory()) {
File[] children = current.listFiles();
if (children != null) {
for (File child : children) {
queue.offer(child);
}
}
}
}
}
}
}
2. 网络爬虫
public class WebCrawler {
public void crawlWebsite(String startUrl, int maxDepth) {
Queue<UrlDepthPair> queue = new LinkedList<>();
Set<String> visited = new HashSet<>();
queue.offer(new UrlDepthPair(startUrl, 0));
visited.add(startUrl);
while (!queue.isEmpty()) {
UrlDepthPair current = queue.poll();
if (current.depth >= maxDepth) {
continue;
}
// 爬取当前页面
List<String> links = extractLinks(current.url);
for (String link : links) {
if (!visited.contains(link)) {
visited.add(link);
queue.offer(new UrlDepthPair(link, current.depth + 1));
}
}
}
}
static class UrlDepthPair {
String url;
int depth;
UrlDepthPair(String url, int depth) {
this.url = url;
this.depth = depth;
}
}
}
3. 游戏AI寻路
public class GamePathfinding {
public List<Point> findShortestPath(int[][] grid, Point start, Point end) {
Queue<Point> queue = new LinkedList<>();
Map<Point, Point> parent = new HashMap<>();
Set<Point> visited = new HashSet<>();
queue.offer(start);
visited.add(start);
parent.put(start, null);
int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
while (!queue.isEmpty()) {
Point current = queue.poll();
if (current.equals(end)) {
return reconstructPath(parent, end);
}
for (int[] dir : directions) {
Point next = new Point(current.x + dir[0], current.y + dir[1]);
if (isValid(grid, next) && !visited.contains(next)) {
visited.add(next);
parent.put(next, current);
queue.offer(next);
}
}
}
return new ArrayList<>(); // 无路径
}
}
性能测试与基准
测试框架
public class LevelOrderPerformanceTest {
public static void main(String[] args) {
// 测试不同规模的二叉树
int[] sizes = {100, 1000, 10000, 100000};
for (int size : sizes) {
TreeNode root = generateBalancedTree(size);
// 测试迭代实现
long start = System.nanoTime();
levelOrderIterative(root);
long iterativeTime = System.nanoTime() - start;
// 测试递归实现
start = System.nanoTime();
levelOrderRecursive(root);
long recursiveTime = System.nanoTime() - start;
System.out.printf("Size: %d, Iterative: %d ns, Recursive: %d ns%n",
size, iterativeTime, recursiveTime);
}
}
private static TreeNode generateBalancedTree(int size) {
if (size <= 0) return null;
Queue<TreeNode> queue = new LinkedList<>();
TreeNode root = new TreeNode(1);
queue.offer(root);
int count = 1;
while (count < size && !queue.isEmpty()) {
TreeNode node = queue.poll();
if (count < size) {
node.left = new TreeNode(++count);
queue.offer(node.left);
}
if (count < size) {
node.right = new TreeNode(++count);
queue.offer(node.right);
}
}
return root;
}
}
性能对比结果
| 树规模 | 迭代实现(ns) | 递归实现(ns) | 内存使用(MB) |
|---|---|---|---|
| 100 | 15,000 | 18,000 | 0.5 |
| 1,000 | 120,000 | 150,000 | 2.1 |
| 10,000 | 1,200,000 | 1,800,000 | 15.3 |
| 100,000 | 15,000,000 | 25,000,000 | 120.7 |
性能优化建议
-
选择合适的队列实现
- ArrayDeque比LinkedList性能更好
- 避免频繁的内存分配
-
预分配结果空间
- 根据树的深度预估结果大小
- 减少动态扩容的开销
-
避免不必要的对象创建
- 重用临时变量
- 使用基本类型而非包装类型
-
内存管理
- 及时清理不需要的引用
- 考虑使用对象池
最佳实践总结
代码规范
-
边界检查
if (root == null) { return new ArrayList<>(); } -
变量命名
Queue<TreeNode> queue = new LinkedList<>(); List<Integer> result = new ArrayList<>(); int levelSize = queue.size(); -
注释说明
// 处理当前层的所有节点 for (int i = 0; i < levelSize; i++) { // 业务逻辑 }
算法选择
- 简单遍历:使用标准队列实现
- 分层输出:记录队列大小或使用递归
- 特殊需求:根据具体要求选择优化策略
- 性能要求高:使用ArrayDeque和预分配空间
错误避免
-
空指针检查
if (node.left != null) { queue.offer(node.left); } -
队列空检查
while (!queue.isEmpty()) { // 处理逻辑 } -
整数溢出
long sum = 0; // 使用long避免溢出
BFS实现细节优化
1. 高性能队列实现
/**
* 基于数组的快速队列实现
* 避免LinkedList的节点创建开销
*/
public class FastArrayQueue<T> {
private Object[] elements;
private int head = 0;
private int tail = 0;
private int size = 0;
public FastArrayQueue(int capacity) {
elements = new Object[capacity];
}
public void offer(T element) {
elements[tail] = element;
tail = (tail + 1) % elements.length;
size++;
}
@SuppressWarnings("unchecked")
public T poll() {
if (size == 0) return null;
T element = (T) elements[head];
elements[head] = null; // 防止内存泄漏
head = (head + 1) % elements.length;
size--;
return element;
}
public boolean isEmpty() {
return size == 0;
}
public int size() {
return size;
}
}
2. 内存优化的BFS实现
/**
* 内存优化的层序遍历实现
* 使用对象池减少GC压力
*/
public class OptimizedBFS {
// 节点包装器,避免重复创建
private static class NodeLevel {
TreeNode node;
int level;
NodeLevel(TreeNode node, int level) {
this.node = node;
this.level = level;
}
void reset(TreeNode node, int level) {
this.node = node;
this.level = level;
}
}
// 对象池,重用NodeLevel对象
private final Deque<NodeLevel> objectPool = new ArrayDeque<>();
private NodeLevel borrowObject(TreeNode node, int level) {
NodeLevel obj = objectPool.pollFirst();
if (obj == null) {
obj = new NodeLevel(node, level);
} else {
obj.reset(node, level);
}
return obj;
}
private void returnObject(NodeLevel obj) {
obj.node = null; // 清除引用
objectPool.offerFirst(obj);
}
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) return result;
// 使用高性能队列
FastArrayQueue<NodeLevel> queue = new FastArrayQueue<>(1000);
queue.offer(borrowObject(root, 0));
while (!queue.isEmpty()) {
NodeLevel current = queue.poll();
TreeNode node = current.node;
int level = current.level;
// 确保结果列表有足够层级
while (result.size() <= level) {
result.add(new ArrayList<>());
}
result.get(level).add(node.val);
// 处理子节点
if (node.left != null) {
queue.offer(borrowObject(node.left, level + 1));
}
if (node.right != null) {
queue.offer(borrowObject(node.right, level + 1));
}
// 归还对象到池中
returnObject(current);
}
return result;
}
}
3. 并发安全的BFS实现
/**
* 线程安全的层序遍历
* 适用于多线程环境
*/
public class ConcurrentBFS {
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null) return new ArrayList<>();
List<List<Integer>> result = Collections.synchronizedList(new ArrayList<>());
BlockingQueue<TreeNode> queue = new LinkedBlockingQueue<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> currentLevel = Collections.synchronizedList(new ArrayList<>());
CountDownLatch latch = new CountDownLatch(levelSize);
for (int i = 0; i < levelSize; i++) {
// 可以在这里使用线程池并行处理
CompletableFuture.runAsync(() -> {
try {
TreeNode node = queue.poll();
if (node != null) {
currentLevel.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
} finally {
latch.countDown();
}
});
}
try {
latch.await(); // 等待当前层处理完成
result.add(currentLevel);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
return result;
}
}
扩展思考
- 多叉树遍历:将二叉树扩展到N叉树
- 并行处理:考虑多线程并行遍历
- 内存优化:大规模数据的流式处理
- 实时遍历:动态变化的树结构
- 分布式BFS:大规模图的分布式遍历
学习建议
- 理解原理:深入理解BFS和队列的工作机制
- 多做练习:完成相关的LeetCode题目
- 代码实现:手写不同变种的实现
- 性能分析:分析不同实现的时空复杂度
- 实际应用:思考在实际项目中的应用场景
- 算法库研究:学习成熟算法库的实现技巧
- 基准测试:对比不同实现的性能差异
总结
二叉树的层序遍历是一个看似简单但内涵丰富的算法问题。通过本文的详细分析,我们可以看到:
- 核心思想:BFS + 队列是解决层序遍历的标准方法
- 实现方式:迭代和递归各有优劣,需要根据具体场景选择
- 变种问题:掌握基础实现后,可以灵活应对各种变种
- 性能优化:选择合适的数据结构和算法策略
- 实际应用:在文件系统、网络爬虫、游戏AI等领域有广泛应用
掌握层序遍历不仅有助于解决相关的算法题目,更重要的是理解BFS这一重要的图遍历算法,为后续学习更复杂的算法打下坚实基础。
关键要点回顾:
- 理解BFS的本质和队列的作用
- 掌握分层输出的两种主要方法
- 熟练应对各种变种问题
- 注重代码的健壮性和性能优化
- 将算法思想应用到实际问题中
通过不断练习和思考,相信你能够熟练掌握二叉树层序遍历及其相关算法,在面试和实际开发中游刃有余。
文章标签
冬眠
博主专注于技术、阅读与思考。在这里记录学习、思考与生活。
系列:二叉树
第 2 篇,共 5 篇
