数据结构与算法 - BFS的核心逻辑:层次遍历与最短路径求解
本文深入解析广度优先搜索(BFS)算法的核心原理与应用。BFS通过队列实现"涟漪式"逐层遍历,特别适合解决层次遍历和最短路径问题。文章包含: BFS基本思想:类比水面波纹扩散 核心数据结构:队列(FIFO) Java实现模板(邻接表表示图) 二叉树层次遍历实战(带层级分组) Mermaid可视化遍历过程 通过清晰的代码示例和图表展示,帮助读者掌握BFS在树和图结构中的应用场景及

👋 大家好,欢迎来到我的技术博客!
💻 作为一名热爱 Java 与软件开发的程序员,我始终相信:清晰的逻辑 + 持续的积累 = 稳健的成长。
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕数据结构与算法这个话题展开,希望能为你带来一些启发或实用的参考。
🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!
文章目录
- 数据结构与算法 - BFS的核心逻辑:层次遍历与最短路径求解 🧠🗺️
数据结构与算法 - BFS的核心逻辑:层次遍历与最短路径求解 🧠🗺️
在计算机科学的广袤天地中,图(Graph)和树(Tree)是两种极其重要的数据结构。它们不仅用于建模现实世界中的关系网络(如社交网络、交通路线、网页链接等),也是解决许多复杂问题的基石。而在图/树的遍历算法中,广度优先搜索(Breadth-First Search, 简称 BFS)以其独特的“逐层扩展”特性,成为处理层次遍历和最短路径问题的首选工具。
本文将深入剖析 BFS 的核心逻辑,从基本思想、实现细节,到在树和图中的典型应用,再到最短路径问题的求解原理。我们将通过大量 Java 代码示例、可视化图表(使用 Mermaid)以及真实场景类比,帮助你彻底掌握 BFS 的精髓。无论你是准备面试的开发者,还是正在学习算法的学生,这篇文章都将为你提供系统而深入的理解。
什么是 BFS?—— 从“涟漪”说起 💧
想象你向平静的湖面扔下一颗石子。水面会以石子落点为中心,一圈一圈地向外扩散波纹。这种“由近及远、层层推进”的现象,正是 BFS 的直观体现。
在 BFS 中,我们从一个起始节点(称为源点)出发,首先访问它所有直接相邻的节点(即距离为 1 的节点),然后再访问这些相邻节点的相邻节点(即距离为 2 的节点),依此类推,直到遍历完整个图或找到目标节点。
✅ BFS 的本质:按“距离”递增的顺序访问节点。
这种特性使得 BFS 在以下两类问题中表现卓越:
- 层次遍历(Level-order Traversal):在树或图中按层访问节点。
- 无权图的最短路径(Shortest Path in Unweighted Graph):因为 BFS 第一次到达某节点时,走的路径一定是最短的。
BFS 的核心数据结构:队列 🧱
BFS 的实现离不开一个关键的数据结构:队列(Queue)。
队列遵循 FIFO(First In, First Out)原则——先进入队列的元素先被处理。这恰好符合 BFS “先访问近的,再访问远的”逻辑。
BFS 算法流程(通用版)
- 将起始节点加入队列,并标记为已访问。
- 当队列非空时:
- 从队列头部取出一个节点(当前节点)。
- 处理该节点(例如打印、记录路径等)。
- 将该节点所有未被访问过的邻居节点加入队列,并标记为已访问。
- 重复步骤 2,直到队列为空。
⚠️ 注意:必须标记已访问节点,否则可能导致无限循环(尤其在图中存在环时)。
Java 实现 BFS 基础框架
下面是一个通用的 BFS 模板(适用于图):
import java.util.*;
public class BFSExample {
// 图用邻接表表示:Map<Integer, List<Integer>>
public static void bfs(Map<Integer, List<Integer>> graph, int start) {
Queue<Integer> queue = new LinkedList<>();
Set<Integer> visited = new HashSet<>();
queue.offer(start);
visited.add(start);
while (!queue.isEmpty()) {
int node = queue.poll();
System.out.println("Visiting node: " + node); // 处理当前节点
// 遍历所有邻居
for (int neighbor : graph.getOrDefault(node, Collections.emptyList())) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.offer(neighbor);
}
}
}
}
public static void main(String[] args) {
// 构建一个简单图:0 -> [1, 2], 1 -> [3], 2 -> [3], 3 -> []
Map<Integer, List<Integer>> graph = new HashMap<>();
graph.put(0, Arrays.asList(1, 2));
graph.put(1, Arrays.asList(3));
graph.put(2, Arrays.asList(3));
graph.put(3, Collections.emptyList());
bfs(graph, 0);
}
}
运行结果:
Visiting node: 0
Visiting node: 1
Visiting node: 2
Visiting node: 3
这个例子清晰地展示了 BFS 的层次性:先访问 0,再访问 1 和 2(同一层),最后访问 3(下一层)。
BFS 在二叉树中的应用:层次遍历 🌳
在二叉树中,BFS 通常被称为层次遍历(Level-order Traversal)。这是面试中的高频题型。
问题:按层打印二叉树节点
给定如下二叉树:
3
/ \
9 20
/ \
15 7
期望输出:[[3], [9,20], [15,7]]
Java 实现(带层级分组)
import java.util.*;
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;
}
}
public class BinaryTreeLevelOrder {
public List<List<Integer>> levelOrder(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;
}
public static void main(String[] args) {
// 构建示例树
TreeNode root = new TreeNode(3);
root.left = new TreeNode(9);
root.right = new TreeNode(20);
root.right.left = new TreeNode(15);
root.right.right = new TreeNode(7);
BinaryTreeLevelOrder solver = new BinaryTreeLevelOrder();
List<List<Integer>> levels = solver.levelOrder(root);
System.out.println(levels); // [[3], [9, 20], [15, 7]]
}
}
🔍 关键技巧:通过
queue.size()获取当前层的节点数量,从而实现按层分组。
Mermaid 可视化:BFS 遍历过程 📊
下面用 Mermaid 图表展示上述二叉树的 BFS 遍历顺序:
- 🟡 节点 3:第 0 层(起始)
- 🔵 节点 9、20:第 1 层
- 🟢 节点 15、7:第 2 层
BFS 严格按照这个顺序访问节点。
BFS 与最短路径:为什么它能保证最短? 🧭
这是 BFS 最强大的特性之一:在无权图(或边权相等的图)
直观理解
假设从起点 S 到终点 T 有两条路径:
- 路径 A:S → A → T(长度 2)
- 路径 B:S → B → C → T(长度 3)
BFS 会先探索所有距离为 1 的节点(A、B),再探索距离为 2 的节点(T、C)。因此,当 BFS 第一次访问到 T 时,走的是路径 A,长度为 2,这正是最短路径。
✅ 关键结论:BFS 第一次访问到某节点时,所经过的路径就是从起点到该节点的最短路径(在无权图中)。
数学证明(简略)
BFS 按距离 d = 0, 1, 2, … 的顺序访问节点。假设存在一条更短的路径未被 BFS 找到,那么该路径上的某个节点应在更早的层级被访问,矛盾。因此 BFS 找到的路径一定是最短的。
实战:LeetCode 经典题 —— 二进制矩阵中的最短路径 🧩
题目(LeetCode 1091):
给定一个 n x n 的二进制矩阵,0 表示可通过,1 表示障碍。从左上角 (0,0) 到右下角 (n-1,n-1),每次可向 8 个方向移动。求最短路径长度。若不可达,返回 -1。
分析
- 这是一个网格图(Grid Graph),每个格子是节点,相邻格子(8方向)是边。
- 边权均为 1(每步代价相同)→ 适用 BFS。
- 需要记录路径长度 → 在队列中存储
(row, col, distance)。
Java 实现
import java.util.*;
public class ShortestPathInBinaryMatrix {
public int shortestPathBinaryMatrix(int[][] grid) {
if (grid == null || grid.length == 0 || grid[0][0] == 1) {
return -1;
}
int n = grid.length;
if (n == 1) return 1;
// 8个方向
int[][] directions = {
{-1,-1}, {-1,0}, {-1,1},
{0,-1}, {0,1},
{1,-1}, {1,0}, {1,1}
};
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{0, 0, 1}); // {row, col, distance}
grid[0][0] = 1; // 标记为已访问(直接修改原数组节省空间)
while (!queue.isEmpty()) {
int[] current = queue.poll();
int row = current[0];
int col = current[1];
int dist = current[2];
for (int[] dir : directions) {
int newRow = row + dir[0];
int newCol = col + dir[1];
if (newRow == n - 1 && newCol == n - 1) {
return dist + 1;
}
if (newRow >= 0 && newRow < n && newCol >= 0 && newCol < n && grid[newRow][newCol] == 0) {
grid[newRow][newCol] = 1; // 标记访问
queue.offer(new int[]{newRow, newCol, dist + 1});
}
}
}
return -1;
}
public static void main(String[] args) {
ShortestPathInBinaryMatrix solver = new ShortestPathInBinaryMatrix();
int[][] grid = {
{0,0,0},
{1,1,0},
{1,1,0}
};
System.out.println(solver.shortestPathBinaryMatrix(grid)); // 输出: 4
}
}
💡 优化技巧:直接修改
grid数组将 0 改为 1 来标记访问,避免额外使用visited数组。
BFS vs DFS:何时选择 BFS? ⚖️
| 特性 | BFS | DFS |
|---|---|---|
| 数据结构 | 队列(Queue) | 栈(Stack)或递归 |
| 空间复杂度 | O(w)(w 为最大宽度) | O(h)(h 为最大深度) |
| 最短路径 | ✅ 无权图中最短路径 | ❌ 不保证最短 |
| 内存消耗 | 宽图时可能很大 | 深图时可能栈溢出 |
| 适用场景 | 层次遍历、最短路径、拓扑排序(部分) | 路径存在性、回溯、连通分量 |
📌 经验法则:
- 要找“最少步数”、“最短距离” → 选 BFS。
- 要找“是否存在路径”、“所有路径” → 可考虑 DFS。
多源 BFS:从多个起点同时开始 🌐
有时问题不是从一个点出发,而是从多个点同时开始 BFS。例如:
- 腐烂的橘子(LeetCode 994):多个腐烂橘子同时感染周围新鲜橘子。
- 地图中的最近距离:多个目标点,求每个点到最近目标的距离。
示例:01 矩阵(LeetCode 542)
给定一个 01 矩阵,对每个 1,计算到最近 0 的距离。
思路:将所有 0 作为起点,同时入队,进行多源 BFS。
public class UpdateMatrix {
public int[][] updateMatrix(int[][] mat) {
int m = mat.length, n = mat[0].length;
Queue<int[]> queue = new LinkedList<>();
int[][] directions = {{0,1},{0,-1},{1,0},{-1,0}};
// 初始化:所有0入队,1设为-1表示未访问
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (mat[i][j] == 0) {
queue.offer(new int[]{i, j});
} else {
mat[i][j] = -1;
}
}
}
while (!queue.isEmpty()) {
int[] cell = queue.poll();
int row = cell[0], col = cell[1];
for (int[] dir : directions) {
int newRow = row + dir[0];
int newCol = col + dir[1];
if (newRow >= 0 && newRow < m && newCol >= 0 && newCol < n && mat[newRow][newCol] == -1) {
mat[newRow][newCol] = mat[row][col] + 1;
queue.offer(new int[]{newRow, newCol});
}
}
}
return mat;
}
}
🌟 多源 BFS 的本质:将多个源点视为“同一层”的起点,后续扩展逻辑不变。
BFS 在图论中的其他应用 🧩
1. 拓扑排序(Topological Sort)
虽然通常用 DFS 实现,但 BFS 也可用于拓扑排序(Kahn 算法):
- 计算每个节点的入度。
- 将入度为 0 的节点入队。
- 依次出队,移除其出边,若邻居入度变为 0,则入队。
2. 连通分量(Connected Components)
在无向图中,BFS 可用于找出所有连通分量:
public void findConnectedComponents(Map<Integer, List<Integer>> graph) {
Set<Integer> visited = new HashSet<>();
int componentId = 0;
for (int node : graph.keySet()) {
if (!visited.contains(node)) {
bfsComponent(graph, node, visited);
System.out.println("Component " + (++componentId) + " done.");
}
}
}
private void bfsComponent(Map<Integer, List<Integer>> graph, int start, Set<Integer> visited) {
Queue<Integer> queue = new LinkedList<>();
queue.offer(start);
visited.add(start);
while (!queue.isEmpty()) {
int node = queue.poll();
for (int neighbor : graph.getOrDefault(node, Collections.emptyList())) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.offer(neighbor);
}
}
}
}
性能分析:时间与空间复杂度 ⏱️
-
时间复杂度:O(V + E)
V:节点数,E:边数。每个节点和每条边最多被访问一次。 -
空间复杂度:O(V)
队列最多存储所有节点(如完全图的第一层),visited集合也需 O(V)。
📊 对于树:E = V - 1 → 时间复杂度 O(V)
常见陷阱与调试技巧 🛠️
- 忘记标记已访问 → 无限循环。
- 在错误时机标记访问:应在入队时标记,而非出队时。
- ❌ 错误:出队后才标记 → 同一节点可能多次入队。
- ✅ 正确:入队时立即标记。
- 边界条件遗漏:如空图、单节点、起点即终点。
- 方向数组写错:尤其在网格问题中,8方向 vs 4方向。
扩展阅读与资源 🔗
如果你希望进一步深入学习 BFS 及其变种,以下资源值得参考:
- BFS on GeeksforGeeks — 详细讲解与图示,代码示例丰富。
- Visualgo - BFS Visualization — 交互式动画演示 BFS 过程,直观理解算法步骤。
- LeetCode BFS Tag — 大量 BFS 相关题目,从易到难。
✅ 所有链接均经过测试,可正常访问(截至 2025 年 10 月)。
总结:BFS 的核心价值 🎯
BFS 不仅仅是一个遍历算法,它是一种按距离扩展的思维模式。掌握 BFS,意味着你拥有了处理以下问题的强大工具:
- 树的层次遍历
- 无权图的最短路径
- 网格中的最少步数问题
- 多源扩散模拟(如火灾蔓延、病毒感染)
- 图的连通性分析
通过本文的系统讲解、Java 代码实现、Mermaid 可视化以及实战例题,相信你已经对 BFS 的核心逻辑有了深刻理解。记住:队列是 BFS 的心脏,层次是 BFS 的灵魂,最短路径是 BFS 的荣耀。
现在,打开你的 IDE,亲手实现一个 BFS 吧!🚀
🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨
更多推荐



所有评论(0)