广度优先搜索(BFS)
最后更新
广度优先搜索逐层探索一个图。它从源节点出发,先访问所有直接相邻的节点,再访问这些节点尚未访问的邻居,依此类推,像一圈圈距离递增的波纹一样向外扩展。点击上方的“播放”,观察它如何从起点一层一层地向外展开。
BFS 使用先进先出的队列,正是队列保证了逐层访问的顺序。因为它按跳数从近到远到达各个节点,BFS 能在无权图中找到最短路径(边数最少)。它对每个节点和每条边各访问一次,所以运行时间是 O(V + E)。
时间和空间复杂度
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间 | O(V + E) | 每个顶点和每条边各访问一次 |
| 空间 | O(V) | 队列加已访问集合,最坏情况包含所有节点 |
| 遍历方式 | 逐层 | 最近的节点优先,一圈一圈向外 |
| 最短路径 | 是(无权图) | 以最少的边到达每个节点 |
分步说明
| 步骤 | 发生了什么 |
|---|---|
| 1 | 把源节点加入队列。 |
| 2 | 取出队首节点,并标记为已访问。 |
| 3 | 查看它的每个邻居。 |
| 4 | 把所有尚未访问、也不在队列中的邻居加入队列。 |
| 5 | 重复,直到队列为空。 |
示例演算
从节点 0 开始遍历这个图,它的边为 0-1、0-2、1-3、2-3、2-4:
| 步骤 | 已访问 | 队列(待访问) |
|---|---|---|
| 开始 | {} | [0] |
出队 0 | {0} | [1, 2] |
出队 1 | {0, 1} | [2, 3] |
出队 2 | {0, 1, 2} | [3, 4] |
出队 3 | {0, 1, 2, 3} | [4] |
出队 4 | {0, 1, 2, 3, 4} | [](完成) |
什么时候使用 BFS
| 适合使用的情况 | 不适合使用的情况 |
|---|---|
| 需要在无权图中找最短路径 | 边带有权重,应改用 Dijkstra 算法 |
| 目标很可能离源节点很近 | 图非常宽,队列可能要存放庞大的待访问节点 |
| 希望按层次顺序探索图 | 只需到达任意一个节点,这时 DFS 占用的内存更少 |
| 要找连通分量或跳数最少的路线 | 需要拓扑排序或环检测,DFS 更合适 |
BFS 与 DFS 对比
两者都以 O(V + E) 遍历所有节点,但访问顺序和所用的数据结构不同。打开深度优先搜索的可视化,把两者放在一起对比。
| 方面 | BFS | DFS |
|---|---|---|
| 数据结构 | 队列(FIFO) | 栈或递归 |
| 顺序 | 逐层(最近的优先) | 沿一条分支深入,再回溯 |
| 最短路径(无权图) | 是,边数最少 | 否,无法保证 |
| 在宽图上的内存 | 高,待访问节点可能很多 | 低,每次只保存一条路径 |
| 最适合 | 最少跳数、连通分量 | 环检测、拓扑排序、回溯 |
Breadth-First Search 代码
简洁、可运行的 Breadth-First Search 实现,提供 Python, JavaScript, Java, C++, C 版本。选择一种语言,复制代码,或在 Coddy 在线编辑器中直接打开。
Breadth-First Search 的 Python 代码
1from collections import deque2
3
4def bfs(graph, start):5 visited = {start}6 queue = deque([start])7 order = []8 while queue:9 node = queue.popleft()10 order.append(node)11 for neighbor in graph[node]:12 if neighbor not in visited:13 visited.add(neighbor) # mark on enqueue, not dequeue14 queue.append(neighbor)15 return order16
17
18graph = {19 "A": ["B", "C"],20 "B": ["D", "E"],21 "C": ["F"],22 "D": [],23 "E": ["F"],24 "F": [],25}26
27print("BFS order:", " -> ".join(bfs(graph, "A")))Breadth-First Search 的 JavaScript 代码
1const graph = {2 A: ["B", "C"],3 B: ["D", "E"],4 C: ["F"],5 D: [],6 E: ["F"],7 F: [],8};9
10function bfs(start) {11 const visited = new Set([start]);12 const queue = [start];13 const order = [];14 while (queue.length > 0) {15 const node = queue.shift(); // Dequeue the oldest node16 order.push(node);17 for (const next of graph[node]) {18 if (!visited.has(next)) {19 visited.add(next);20 queue.push(next);21 }22 }23 }24 return order;25}26
27console.log("BFS from A:", bfs("A").join(" -> "));Breadth-First Search 的 Java 代码
1import java.util.ArrayDeque;2import java.util.List;3import java.util.Queue;4
5public class Main {6 public static void main(String[] args) {7 List<List<Integer>> adj = List.of(8 List.of(1, 2), // neighbors of 09 List.of(0, 3, 4), // neighbors of 110 List.of(0, 5),11 List.of(1),12 List.of(1, 5),13 List.of(2, 4)14 );15 boolean[] visited = new boolean[adj.size()];16 Queue<Integer> queue = new ArrayDeque<>();17 visited[0] = true;18 queue.add(0);19
20 StringBuilder order = new StringBuilder("BFS from 0:");21 while (!queue.isEmpty()) {22 int node = queue.poll();23 order.append(" ").append(node);24 // Mark neighbors when enqueued so nothing enters twice25 for (int next : adj.get(node)) {26 if (!visited[next]) {27 visited[next] = true;28 queue.add(next);29 }30 }31 }32 System.out.println(order);33 }34}Breadth-First Search 的 C++ 代码
1#include <iostream>2#include <queue>3#include <vector>4
5void bfs(int start, const std::vector<std::vector<int>>& adj) {6 std::vector<bool> visited(adj.size(), false);7 std::queue<int> frontier;8 visited[start] = true;9 frontier.push(start);10 // Visit nodes level by level11 while (!frontier.empty()) {12 int node = frontier.front();13 frontier.pop();14 std::cout << node << " ";15 for (int next : adj[node]) {16 if (!visited[next]) {17 visited[next] = true;18 frontier.push(next);19 }20 }21 }22}23
24int main() {25 // 6-node undirected graph as an adjacency list26 std::vector<std::vector<int>> adj = {27 {1, 2}, // 028 {0, 3, 4}, // 129 {0, 5}, // 230 {1}, // 331 {1, 5}, // 432 {2, 4}, // 533 };34 std::cout << "BFS from node 0: ";35 bfs(0, adj);36 std::cout << "\n";37 return 0;38}Breadth-First Search 的 C 代码
1#include <stdbool.h>2#include <stdio.h>3
4#define N 65
6void bfs(int start, const int adj[N][N]) {7 bool visited[N] = {false};8 int queue[N]; // fixed-size queue: head chases tail9 int head = 0, tail = 0;10 visited[start] = true;11 queue[tail++] = start;12 // Visit nodes level by level13 while (head < tail) {14 int node = queue[head++];15 printf("%d ", node);16 for (int next = 0; next < N; next++) {17 if (adj[node][next] && !visited[next]) {18 visited[next] = true;19 queue[tail++] = next;20 }21 }22 }23}24
25int main(void) {26 // 6-node undirected graph as an adjacency matrix27 int adj[N][N] = {28 {0, 1, 1, 0, 0, 0},29 {1, 0, 0, 1, 1, 0},30 {1, 0, 0, 0, 0, 1},31 {0, 1, 0, 0, 0, 0},32 {0, 1, 0, 0, 0, 1},33 {0, 0, 1, 0, 1, 0},34 };35 printf("BFS from node 0: ");36 bfs(0, adj);37 printf("\n");38 return 0;39}广度优先搜索常见问题
BFS 的时间复杂度是多少?
O(V + E),其中 V 是顶点数,E 是边数,因为它对每个顶点访问一次,对每条边检查一次。队列和已访问集合占用 O(V) 的空间。BFS 能找到最短路径吗?
BFS 和 DFS 有什么区别?
什么时候该用 BFS 而不是 Dijkstra 算法?
O(V + E) 时间内找到边数最少的路径。当边的权重各不相同时,才需要 Dijkstra 算法;在带权图上直接运行 BFS,得到的是跳数最少的路径,而不是代价最低的路径。为什么 BFS 需要已访问集合?
图中可能有环,如果没有已访问集合,BFS 会反复把同一个节点加入队列,陷入无限循环。在节点第一次入队时(而不是出队时)就标记它,还能防止同一个节点被不同的邻居重复加入队列。
应该在入队时还是出队时标记节点为已访问?
在入队时标记。如果等到出队时才标记,一个节点在被处理之前,可能会被不同的邻居多次加入队列,浪费内存和时间。入队时标记能保证每个节点恰好进入队列一次。