BFS,breadth-first search,广度优先搜索。以同心圆的形式依次遍历所有的节点,每次迭代时,先处理所有与当前节点相邻的未访问的节点,然后再访问与当前节点距离为2的所有未访问的节点,再访问与当前节点距离为3的所有未访问的节点,以此类推
因为是以同心圆的形式遍历,所以这就需要用到一个数据结构——队列
一些特殊情况
首先我们需要考虑到一些特殊情况:
- 某些图存在环路,因此我们需要避免因为环路导致的死循环,即重复遍历相同的节点。
- 解决方案就是,添加一个标志位mark,未遍历的节点mark为0
- 某些图并不是所有的节点都连接在一起,即从一个节点出发可能无法遍历所有的节点。
- 解决方案就是,在一次遍历结束时,检查标志数组mark[],通过查看其所有的mark是否为0,来判断是否遍历完所有的节点。
解决完这些特殊情况之后,那么接下来就进入到了正式环节:
先上伪代码
因为伪代码没有其他语言的语法限制,能够有助于我们更好地将注意力放在算法上,所以这里我们先用伪代码来讲述一下主要思想:
algorithm BFS(G)
//实现给定图G的广度优先搜索
//输入:图G=<V, E>,其中V为G中所有的节点集合,E为G中所有的边集合
//输出:图G的节点,按照BFS遍历访问到的先后顺序
count <- 0
for 每个属于V的v do
mark[v] <- 0 //将每个节点的标志位都赋初值为0,表示未被访问
for 每个属于V的v do
if mark[v] = 0
bfsVisit(v)
bfsVisit(v)
//访问所有的和v相邻的未被访问的节点,然后通过全局变量count,根据访问的先后顺序,对其赋上相应的值
visit(v) //这里对visit()函数不做介绍,可以根据情况进行处理
Initialize(Q) //初始化队列Q
count <- count + 1
mark[v] <- count
Enqueue(Q, v) //将节点v入队
while 队列不为空 do
x <- Dequeue(Q) //队头元素出队
for 与节点x相邻的节点w do
if mark[w] = 0
visit(w)
count <- count + 1
mark[w] <- count
Enqueue(Q, w)
由以上代码便实现了BFS,接下来只需要使用某一种特定的高级语言来实现即可。
在实现之前,我们先来探讨一下其中的一些细节:
1.以上代码是否实现了遍历所有节点的要求呢?
首先在BFS()中,我们在调用bfsVisit()函数之前,是用了一个循环遍历所有的节点,然后若一个节点未被遍历,则会调用bfsVisit()函数,随后将与其相连接的节点全部遍历完,因此能够实现遍历所有的节点。
for 每个属于V的v do
if mark[v] = 0
bfsVisit(v)
2.以上的代码是否避免了重复遍历相同的节点呢?
在bfsVisit()函数中,遍历队头元素的相邻节点时,首先会判断该节点对应的mark是否为0,即是否未被访问过,若是则将其入队,若已经被访问过了,则跳过,因此能够避免重复遍历。
while 队列不为空 do
x <- Dequeue(Q) //队头元素出队
for 与节点x相邻的节点w do
if mark[w] = 0
visit(w)
count <- count + 1
mark[w] <- count
Enqueue(Q, w)
3.在bfsVisit()函数中的while循环中,为什么先访问(visit(w))后入队?
若先入队后访问,则入队的该节点对应的mark为0,那么等到下次出队的元素与其相邻,则由于mark为0,该节点又会入队,从而导致队列中出现了重复的元素,进而导致重复遍历。
图解
看过伪代码之后,可能还是有些抽象,那么接下来再看看图解:
上图中,默认起点为S,然后运用BFS遍历整个图,则队列的变化如下所示(其中,蓝色表示已经被访问过,红色表示未被访问过,同时,以S为例,相邻节点为A和C,入队的顺序为A、C,即按照字母表的顺序入队)
因此,BFS入队的顺序为SACBDFEGH,出队的顺序为SACBDFEGH,遍历后的顺序为SACBDFEGH。
可以看到因为队列“先进先出”(FIFO)的特性,所以入队、出队和遍历的顺序都是一致的。
代码实现
理论讲完之后,接下来就是实践了,这里我通过C++来实现BFS。
首先根据上图构造相对应的邻接矩阵,以及初始化mark[]数组:
int Matrix[9][9] = {
{1, 1, 0, 1, 0, 0, 0, 0, 0},
{1, 1, 1, 0, 1, 0, 0, 0, 0},
{0, 1, 1, 0, 0, 1, 0, 0, 0},
{1, 0, 0, 1, 1, 0, 1, 0, 0},
{0, 1, 0, 1, 1, 1, 0, 1, 0},
{0, 0, 1, 0, 1, 1, 0, 0, 1},
{0, 0, 0, 1, 0, 0, 1, 1, 0},
{0, 0, 0, 0, 1, 0, 1, 1, 1},
{0, 0, 0, 0, 0, 1, 0, 1, 1}
}; //这里我将SABCDEFGH依次定义为012345678,因此BFS遍历之后的结果应该为013246578
int mark[9] = { 0 };
然后构造BFS函数:
void bfsVisit(int v) {
cout << v << endl; //这里我只是输出了其对应的下标,并没有做其他的操作,如需要添加多余的操作,可以自行添加
mark[v] = 1;
queue<int> q; //这里我使用了C++自带的队列数据结构,需要导入<queue>函数库
q.push(v);
while (!q.empty()) {
int k = q.front(); //取出队头元素
q.pop();
for (int i = 0; i < 9; i++) {
if (Matrix[k][i] == 1 && mark[i] == 0) {
cout << i << endl;
mark[i] = 1;
q.push(i);
}
}
}
}
void BFS() {
for (int i = 0; i < 9; i++) {
if (mark[i] == 0) {
bfsVisit(i);
}
}
}
然后在main()函数中调用BFS():
int main() {
BFS();
return 0;
}
运行结果为:
如图所示,我们成功遍历该图,并得到了预期的遍历顺序。
效率分析
时间复杂度取决于图的存储方式,若是采用邻接矩阵,则时间复杂度为O(|V|2);若是采用邻接链表,则时间复杂度为O(|V|+|E|)
BFS的应用
1.检查图的连通性
从任意节点开始使用BFS遍历,若一次遍历结束后,所有节点的mark都不为0,则说明此图是连通的,否则此图是非连通的。
2.检查图的无环性
BFS遍历时,若发现某一个节点和一个已经被访问过的非父节点相邻,则说明此图存在环路,否则无环路。
3.可以求出给定两个节点之间的最短路径
当然,前提是两个节点是连通的,那么我们可以通过BFS的原理思想知道,每次都是找相邻的节点遍历(这里不考虑边的权重),其思想就是贪心思想,因此,通过BFS遍历后的任意两个节点之间的距离一定是最短的。
参考资料
《算法设计与分析基础》第三版以及老师的课件