核心思想
BFS(Breadth First Search,广度优先搜索)从一个顶点出发,先访问离它最近的顶点,再逐层向外扩展。
它具有两个特点:
- 一层一层访问
- 先发现的顶点先处理
因此,BFS 使用**队列(Queue)**实现。
基本过程
以下图为例:
和二叉树的层级遍历还挺像。
A
/ \
B C
/ \ \
D E F
从 A 开始进行 BFS,访问顺序为:
A → B → C → D → E → F
对应的访问层次为:
第 0 层:A
第 1 层:B C
第 2 层:D E F
实现要点
队列
BFS 的基本流程如下:
- 将起点标记为已访问并入队。
- 取出队首顶点并访问。
- 将该顶点所有未访问的邻接点标记并入队。
- 重复以上过程,直到队列为空。
visited 数组
visited[i] = true
表示顶点 i 已经被访问过,主要用于:
- 防止顶点被重复访问
- 防止在有环图中无限循环
例如:
A — B
| |
C — D
如果没有 visited,可能不断循环:
A → B → D → C → A → ...
重点:顶点应在入队时标记为已访问,避免同一顶点被多个邻接点重复加入队列。
非连通图
从一个顶点执行一次 BFS,只能访问该顶点所在的连通分量。要遍历整个图,需要在外层依次检查所有顶点:
for (每个顶点 v)
{
if (!visited[v])
BFS(G, v);
}
对于无向图,调用 BFS 的次数等于图的连通分量数。
复杂度
设顶点数为 ,边数为 :
| 存储方式 | 时间复杂度 | 原因 |
|---|---|---|
| 邻接表 | 每个顶点访问一次,每条边检查有限次 | |
| 邻接矩阵 | 对每个顶点都要扫描矩阵的一整行 |
visited 数组和队列都需要额外空间,因此空间复杂度为 。
最短路径性质
对于无权图或所有边权相同的图,BFS 可以求出起点到其他顶点的最短路径长度。
因为 BFS 是逐层搜索:
第 0 层:距离 0
第 1 层:距离 1
第 2 层:距离 2
第 3 层:距离 3
因此,一个顶点第一次被 BFS 访问时,对应的路径就是边数最少的路径。
注意: BFS 一般不能直接解决边权不同的最短路径问题。
常见用途
- 图的遍历
- 判断图是否连通
- 求无权图最短路径
- 求顶点之间最少经过多少条边
- 求连通分量
- 层序扩展问题
- 树的层序遍历
BFS 与 DFS 对比
| BFS | DFS |
|---|---|
| 广度优先 | 深度优先 |
| 一层一层搜索 | 一条路尽量走到底 |
| 队列 | 栈 / 递归 |
| 适合无权最短路径 | 适合回溯、连通性等 |
| 空间可能较大 | 搜索深度可能较大 |
代码
代码部分与树的层序遍历几乎一样,但需要注意使用 visited 数组。
// 邻接表方式
void BFS(const ALGraph& G, const int start) {
bool visited[MAXV] = {false};
std::queue<int> q;
q.push(start);
visited[start] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
std::cout << u << " ";
ArcNode* p = G.vertices[u].first;
while (p != nullptr) {
int v = p->adjvex;
if (!visited[v]) {
q.push(v);
visited[v] = true;
}
p = p->next;
}
}
}
总结
- 核心数据结构:队列
- 标记时机:顶点入队时
- 邻接表时间复杂度:
- 邻接矩阵时间复杂度:
- 空间复杂度:
- 核心优势:求无权图的最短路径
- 遍历非连通图:在 BFS 外层再遍历所有顶点
其实我还没有看网课,
今天早上把代码梳理消化了一遍,
明天再去把知识点记一记,顺便继续DFS。
今天的学习任务完成的还算比较快,不到19:00就结束了。
不过数学进度还是慢啊,怎么感觉题目这么多新知识。
有点麻烦。 ╮(╯▽╰)╭
