BFS(广度优先搜索)

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遍历后的任意两个节点之间的距离一定是最短的。

参考资料

《算法设计与分析基础》第三版以及老师的课件

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花无凋零之时

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值