BFS算法(队列)
BFS(Breadth-First Search,广度优先搜索)是一种用于遍历或搜索树、图等数据结构的经典算法。其核心思想是:从起始节点出发,优先访问当前节点的所有未访问邻接节点(“广度优先”),待当前层节点全部访问完毕后,再逐层深入访问下一层节点,直至遍历所有可达节点。
- 特点是“逐层扩散”,适合处理无权图的最短路径、层次关系等问题。
- 与DFS的“一路深入”不同,BFS通过“横向扩展”保证首次访问节点时,路径长度是最短的(针对无权图)。
一、BFS的应用场景
BFS的“逐层遍历”特性使其在以下场景中具有天然优势:
- 无权图最短路径:求两点间的最短路径长度(或具体路径),因首次访问节点即通过最短路径到达。
- 层次遍历:树的层序遍历(如二叉树按层输出节点)、图的层级关系分析。
- 判断最多的潜在转发数:最大到第l层
- 连通性判断:与DFS类似,可用于判断图中两点是否连通、统计连通分量个数。
- 判断连通分量个数:被占领一个城市后的修路问题
- 拓扑排序:在有向无环图(DAG)中,结合“入度表”实现拓扑排序(适用于任务调度、依赖关系分析)。
- 扩散类问题:如“病毒扩散的最小时间”“迷宫中离起点最近的出口”等。
二、BFS的原理
BFS的遍历过程类似于“水波扩散”:从起点(石子落水点)出发,水波一圈圈向外扩散,先覆盖最近的区域,再逐步扩展到远处。
- 核心数据结构:队列(Queue)。队列的“先进先出(FIFO)”特性完美匹配BFS的“逐层访问”需求——先入队的节点(当前层)优先被处理,其邻接节点(下一层)按顺序后入队。
- 访问标记:必须通过布尔数组(或哈希表)记录已访问节点,避免重复入队和死循环(尤其图中存在环时)。
- 实现方式:BFS通常以非递归方式实现(队列手动模拟);递归方式虽可行,但因依赖栈结构,会失去“逐层遍历”的直观性,且易因深度过大导致栈溢出,故不常用。
通用模版
1. 非递归版本(推荐)
非递归是BFS的标准实现方式,通过手动维护队列控制遍历流程:
BFS(起点s) {
// 1. 初始化:访问标记数组、队列
初始化visited数组为false;
queue<int> q; // 存储待访问节点
// 2. 起点入队并标记为已访问(入队时标记,避免重复入队)
q.push(s);
visited[s] = true;
// 3. 队列非空时,循环处理节点
while (!q.empty()) {
// 3.1 取出队首节点u(当前层节点)
int u = q.front();
q.pop();
// 3.2 处理当前节点u(如输出、记录路径、更新距离等)
处理u;
// 3.3 遍历u的所有邻接节点v(下一层节点)
for (所有从u出发能到达的节点v) {
if (!visited[v]) { // 仅处理未访问的节点
visited[v] = true; // 标记为已访问
q.push(v); // 入队,等待下一层处理
}
}
}
}
队列的常见方法
- 判断队列是否为空:
q.empty(),是空则返回true,常见操作为while(!q.empty())用来循环队列 - 进入队列:
q.puah() - 删除队首元素:
q.pop() - 取出队首元素
q.front()
2. 递归版本(不常用)
递归实现需额外传递“当前层节点集合”,模拟队列的FIFO特性,实用性较低:
// 辅助函数:递归处理当前层的所有节点
void bfsRecursive(vector<vector<int>>& adj, vector<bool>& visited, queue<int>& q) {
if (q.empty()) return; // 队列空,递归终止
// 取出队首节点u并处理
int u = q.front();
q.pop();
处理u;
// 邻接节点入队
for (int v : adj[u]) {
if (!visited[v]) {
visited[v] = true;
q.push(v);
}
}
// 递归处理下一层节点(队列中剩余节点)
bfsRecursive(adj, visited, q);
}
// 调用入口
BFS(起点s) {
初始化visited数组为false;
queue<int> q;
q.push(s);
visited[s] = true;
bfsRecursive(adj, visited, q); // 启动递归
}
三、BFS的实现步骤(以图为例)
以邻接表存储图(适用于稀疏图,效率高于邻接矩阵)为例,详细说明BFS的实现流程:
步骤1:准备数据结构
- 图的存储:用
vector<vector<int>> adj表示邻接表,adj[u]存储节点u的所有邻接节点。 - 访问标记:用
vector<bool> visited标记节点是否已访问,初始化为false。 - 队列:用
queue<int>存储待访问节点,控制遍历顺序。 - (可选)距离记录:若需求是“最短路径”,可增加
vector<int> dist数组,dist[v]表示从起点到v的最短距离,初始化为-1(未访问),起点dist[s] = 0。
步骤2:核心逻辑(非递归)
- 初始化起点:将起点
s入队,标记visited[s] = true,若记录距离则dist[s] = 0。 - 处理队列节点:
- 弹出队首节点
u,执行具体处理(如输出、计算距离)。 - 遍历
u的所有邻接节点v:- 若
v未访问,标记visited[v] = true,入队; - 若记录距离,
dist[v] = dist[u] + 1(因u是v的前一层节点,距离+1即为最短路径)。
- 若
- 弹出队首节点
- 循环终止:队列空时,所有可达节点已遍历完毕。
步骤3:关键特性解释(为何能求无权图最短路径?)
BFS的“逐层遍历”保证:首次访问节点v时,必然是通过从起点到v的最短路径到达的。例如:
- 起点
s为第0层,dist[s] = 0; s的邻接节点为第1层,dist = 1(最短路径长度1);- 第1层节点的邻接节点(未访问)为第2层,
dist = 2(最短路径长度2); - 后续层级以此类推,故
dist[v]即为起点到v的最短距离。
四、代码示例(C++实现)
以与DFS示例相同的无向图为例(节点0-4),便于对比两种算法的差异:
0 连接 1、2
1 连接 0、3、4
2 连接 0
3 连接 1
4 连接 1
1. 非递归实现(基础遍历+最短路径)
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
// 邻接表存储图
vector<vector<int>> adj;
// 访问标记数组
vector<bool> visited;
// 距离数组:dist[v]表示起点到v的最短距离
vector<int> dist;
// 非递归BFS:从起点start遍历,并计算最短距离
void bfs(int start) {
int n = adj.size();
visited.assign(n, false); // 初始化访问标记
dist.assign(n, -1); // 初始化距离为-1(未访问)
queue<int> q;
// 起点入队
q.push(start);
visited[start] = true;
dist[start] = 0; // 起点到自身距离为0
cout << "BFS遍历结果(非递归):";
while (!q.empty()) {
// 取出队首节点并处理
int u = q.front();
q.pop();
cout << u << " ";
// 遍历邻接节点
for (int v : adj[u]) {
if (!visited[v]) {
visited[v] = true;
dist[v] = dist[u] + 1; // 最短距离+1
q.push(v);
}
}
}
cout << endl;
}
int main() {
// 初始化图(5个节点:0-4)
int n = 5;
adj.resize(n);
// 添加无向边(双向存储)
adj[0].push_back(1);
adj[0].push_back(2);
adj[1].push_back(0);
adj[1].push_back(3);
adj[1].push_back(4);
adj[2].push_back(0);
adj[3].push_back(1);
adj[4].push_back(1);
// 从节点0开始BFS
bfs(0);
// 输出起点0到各节点的最短距离
cout << "起点0到各节点的最短距离:" << endl;
for (int i = 0; i < n; ++i) {
cout << "0 → " << i << ":" << dist[i] << endl;
}
return 0;
}
输出结果:
BFS遍历结果(非递归):0 1 2 3 4
起点0到各节点的最短距离:
0 → 0:0
0 → 1:1
0 → 2:1
0 → 3:2
0 → 4:2
2. 递归实现(演示用,不推荐)
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
vector<vector<int>> adj;
vector<bool> visited;
// 递归辅助函数
void bfsRecursive(queue<int>& q) {
if (q.empty()) return;
// 处理队首节点
int u = q.front();
q.pop();
cout << u << " ";
// 邻接节点入队
for (int v : adj[u]) {
if (!visited[v]) {
visited[v] = true;
q.push(v);
}
}
// 递归处理下一层
bfsRecursive(q);
}
// 递归BFS入口
void bfs(int start) {
int n = adj.size();
visited.assign(n, false);
queue<int> q;
q.push(start);
visited[start] = true;
cout << "BFS遍历结果(递归):";
bfsRecursive(q);
cout << endl;
}
int main() {
// 图初始化与非递归示例相同
int n = 5;
adj.resize(n);
adj[0].push_back(1);
adj[0].push_back(2);
adj[1].push_back(0);
adj[1].push_back(3);
adj[1].push_back(4);
adj[2].push_back(0);
adj[3].push_back(1);
adj[4].push_back(1);
bfs(0); // 输出:0 1 2 3 4
return 0;
}
五、时间复杂度
BFS的时间复杂度与DFS一致,均取决于图的节点数V和边数E:
- 图:每个节点仅入队一次(O(V)),每条边仅被处理一次(O(E)),总时间复杂度为 O(V + E)。
- 树:树是特殊的无环图,边数
E = V - 1,时间复杂度简化为 O(V)(如二叉树的层序遍历)。
六、BFS与DFS的核心差异对比
为帮助理解两种算法的适用场景,下表总结关键差异:
| 维度 | BFS | DFS |
|---|---|---|
| 核心思想 | 逐层扩散(广度优先) | 一路深入(深度优先) |
| 数据结构 | 队列(FIFO) | 栈/递归(LIFO) |
| 路径特性 | 首次访问节点即最短路径(无权图) | 不保证最短路径,需遍历所有路径对比 |
| 空间复杂度 | 取决于最宽层的节点数(可能较高) | 取决于最深层的节点数(递归易溢出) |
| 典型应用 | 无权图最短路径、层次遍历 | 回溯法(排列/子集)、连通分量 |
总结
BFS的核心是**“逐层遍历”,通过队列的FIFO特性控制节点访问顺序,其最大优势是能高效求解无权图的最短路径**和层次关系问题。在实现时,需注意“入队时标记访问”以避免重复入队,非递归版本是工程中的首选实现方式。
掌握BFS与DFS的差异,可根据具体问题场景(如“求最短路径”选BFS,“求所有路径”选DFS)灵活选择算法。
1058

被折叠的 条评论
为什么被折叠?



