《算法与数据结构》第七章[算法2]:广度优先搜索(BFS) - 指南

news/2025/10/26 12:12:20/文章来源:https://www.cnblogs.com/yxysuanfa/p/19166671

1、广度优先遍历

  聪明的你在上一次废弃地下迷宫通过DFS策略成功找到了宝藏,可是不难发现,这个策略是因为只有一人去,所以我们需要找一条路都走,那假设我们拥有神奇的分身术呢?比如在电影复仇者联盟中奇异博士就做到将自己分成多个分身去与灭霸缠斗,这样一来,我们就不用在遇到一个暗室时只挑一条通道进入了,我们不做选择,而是全都要,这样我们就有了如下策略:

  • 每进入一个新的暗室,你(或者分身)就用粉笔在门口做一个记号,表示你已经来过这里了,防止以后再走到这里时重复探索。
  • 每进入一个暗室,先用手电筒照亮这个暗室,看看这里有没有宝藏。
  • 如果这个暗室没有宝藏,你就会查看这个暗室的所有通道,有多少通道就创建多少分身,然后让分身去逐个探索,每个分身进入一个通道,进入下一个暗室,继续进行上述操作。
  • 如果进入的暗室没有通道了,或者所有通道都已经被探索过了(即通道通向的暗室已经被你做过记号了),那你就会结束这个分身的探索,分身消失。
  • 如果宝藏被找到,或者发现所有暗室都被探索过了(即所有暗室的门口都有你的记号了),那你就会结束探索,离开迷宫。

  这种"层层推进,不漏一处"的搜索策略,就是广度优先遍历中"广度"的体现。在这个方法中,我们优先检查离起点近的所有位置,然后再逐渐向外扩展,就像水波一样从出发结点向四周扩散,确保不会错过任何一个可能的位置。

广度优先遍历(Breadth-First Search, BFS):是一种优先广泛探索图中所有相邻节点的遍历策略

  有些同学会发现不同于DFS,我们不再有返回上一顶点,而是让分身走完后就消失了,这样一来我们就不会再用到递归了,那如何实现呢?我们先来看看过程,试着找一下规律,在此之前,我们要体现层层推进,就需要将每一个顶点的邻接点都作为其下一层,我们发现它就会变成一个类似树的结构,我们来看看:

图1:一个图

图1:一个图

  这是我将DFS中我们用于探索遍历过程的图转化为层次分明的类树形结构,我们发现因为图中边关系非常合适,我们这个图实际上是一棵树,同时也不难发现,该棵树的前序遍历结果即为我们当时找出的DFS遍历结果,也正说明我们提出DFS是类似与树的前序遍历的。那BFS呢?我们来看看BFS的遍历过程:

图2:BFS遍历过程

图2:BFS遍历过程

  我们发现这不就是这棵树的层序遍历吗?确实,若果我们前面说DFS是类似于树的前序遍历,那么BFS就是类似于树的层序遍历。可是我们并没有具体探索层序遍历的实现,现在该补上了。

  既然BFS不会用到递归,那么它跟栈也就不会有太大关系了,我们先来看看它的具体顺序,如图59中的过程产生的结果应该是:

v3→v4→v5→v1→v2v_3 \rightarrow v_4 \rightarrow v_5 \rightarrow v_1 \rightarrow v_2v3v4v5v1v2

  我们可以看到,它的策略是:先访问当前顶点,然后逐个访问与当前顶点同一层的其余顶点,当本层所有顶点都被访问后,再从本层最开始的顶点开始,逐个访问其邻接点,再逐个访问本层其余顶点的邻接点,重复此过程,直到所有顶点都被访问为止。

  我们发现,不同于DFS在选择一个未被访问的邻接顶点进行探索,并不关心当前顶点后续的邻接顶点,即只关心最新到达的顶点;BFS则是关心当前顶点的所有邻接顶点,即先来的(同一层的顶点)我就先处理,后来的等我所有需要先处理的处理结束(同一层顶点都处理完了)后再处理,那这样一来,不就符合“先进先出”的特点了吗。那我们就发现BFS跟栈确确实实是没有关系的,而是跟我们在栈之后所学的队列有关。

  现在大家就理解了“队列”一节中我所说队列的应用实例我们当时“还不到学习的时候”的含义了,队列的其中一个应用实例就是BFS。

  同样,我们先给出循环队列及涉及到的操作定义:

#define MAXSIZE 100 // 队列的最大长度
typedef int ElemType; // 队列元素类型
typedef struct {
ElemType data[MAXSIZE]; // 存储队列元素的数组
int front; // 队头指针
int rear; // 队尾指针
} SqQueue;
// 初始化队列
void InitQueue(SqQueue *Q)
{
Q->front = 0;
Q->rear = 0;
}
// 判断队列是否为空
int QueueEmpty(SqQueue *Q)
{
return Q->front == Q->rear;
}
// 入队操作
int EnQueue(SqQueue *Q, ElemType e)
{
if ((Q->rear + 1) % MAXSIZE == Q->front)
{
return 0; // 队满情况
}
Q->data[Q->rear] = e;
Q->rear = (Q->rear + 1) % MAXSIZE;
return 1; // 入队成功
}
// 出队操作
int DeQueue(SqQueue *Q, ElemType *e)
{
if (QueueEmpty(Q))
{
return 0; // 队空情况
}
*e = Q->data[Q->front];
Q->front = (Q->front + 1) % MAXSIZE;
return 1; // 出队成功
}

  我们再给出邻接矩阵存储结构的图的BFS的队列实现代码:

#define MAXVEX 100 // 最大顶点数
int visited[MAXVEX]; // 访问标志数组
void BFS(GraphAdjMatrix G, int i)  // 从顶点i出发进行广度优先遍历
{
SqQueue Q;
InitQueue(&Q);  // 初始化队列
EnQueue(&Q, i); // 将起始顶点入队
visited[i] = 1; // 标记当前顶点已访问
while (!QueueEmpty(&Q)) // 队列不为空
{
DeQueue(&Q, &i); // 出队
printf("%c ", G.vexs[i]); // 访问顶点
for (int j = 0; j < G.numVertexes; j++) // 查找当前顶点的所有邻接点
{
if (G.arc[i][j] == 1 && !visited[j]) // 若邻接点未被访问
{
visited[j] = 1; // 标记为已访问
EnQueue(&Q, j); // 入队
}
}
}
}
void BFSTraverse(GraphAdjMatrix G) // 广度优先遍历图
{
for (int i = 0; i < G.numVertexes; i++)
{
visited[i] = 0; // 初始化访问标记数组
}
for (int i = 0; i < G.numVertexes; i++) // 对每个顶点进行检查
{
if (!visited[i]) // 若顶点未被访问
{
BFS(G, i); // 从该顶点出发进行BFS
}
}
}

  可以看到,大体上跟DFS栈实现的代码结构是类似的,不同之处在于使用了队列,还有一点是我们对顶点做标记的时机不同,DFS是在入栈时做标记,而BFS是在入队时做标记,这样就避免了重复入队。不懂的话没有关系,我们接下来通过一个例子来具体分析一下BFS的过程。

  再给出邻接表的BFS代码:

#define MAXVEX 100 // 最大顶点数
int visited[MAXVEX]; // 访问标志数组
void BFS(GraphAdjList G, int i)  // 从顶点i出发进行广度优先遍历
{
SqQueue Q;
InitQueue(&Q);  // 初始化队列
EnQueue(&Q, i); // 将起始顶点入队
visited[i] = 1; // 标记当前顶点已访问
while (!QueueEmpty(&Q)) // 队列不为空
{
DeQueue(&Q, &i); // 出队
printf("%c ", G.vexs[i].data); // 访问顶点
EdgeNode *p = G.vexs[i].firstedge; // 获取当前顶点的邻接点链表头指针
while (p) // 遍历邻接点链表
{
int j = p->adjvex; // 邻接点的索引
if (!visited[j]) // 若邻接点未被访问
{
visited[j] = 1; // 标记为已访问
EnQueue(&Q, j); // 入队
}
p = p->next; // 移动到下一个邻接点
}
}
}
void BFSTraverse(GraphAdjList G) // 广度优先遍历图
{
for (int i = 0; i < G.numVertexes; i++)
{
visited[i] = 0; // 初始化访问标记数组
}
for (int i = 0; i < G.numVertexes; i++) // 对每个顶点进行检查
{
if (!visited[i]) // 若顶点未被访问
{
BFS(G, i); // 从该顶点出发进行BFS
}
}
}

2、广度优先遍历过程

  我们还是通过一个例子来具体分析一下BFS的过程,假设我们有如下图所示的图:

图3:一个图

图3:一个图

  我们从调用BFSTraverse(G)开始进行广度优先遍历,推出遍历的过程和结果。先初始化所有所需元素,如visited数组,队列等。

图4:BFS遍历过程1

图4:BFS遍历过程1

  同样我们前面的代码不再看,直接从BFS(G, 0)开始,即从顶点v0v_0v0开始。

图5:BFS遍历过程2

图5:BFS遍历过程2

  首先我们将v0v_0v0入队并标记为已访问。

图6:BFS遍历过程3

图6:BFS遍历过程3

  接下来进入循环,判断队列不为空,将队中第一个元素即v0v_0v0出队,出队后先进行访问(输出),然后我们就又看到了一个循环:

for (int j = 0; j < G.numVertexes; j++) // 查找当前顶点的所有邻接点
{
if (G.arc[i][j] == 1 && !visited[j]) // 若邻接点未被访问
{
visited[j] = 1; // 标记为已访问
EnQueue(&Q, j); // 入队
}
}

  可以看到与DFS相同的是,循环变量j均是从0开始,也就是说,我们在同一层的顶点搜索策略也是从下标最小开始的,然后我们同样看G.arc[0]=[0,1,0,0,0,0]G.arc[0]=[0,1,0,0,0,0]G.arc[0]=[0,1,0,0,0,0],也就是说,完整的循环后只有j=1时符合条件,换言之,就是我们将v1v_1v1标记并入队,如下:

图7:BFS遍历过程4

图7:BFS遍历过程4

for循环结束后,我们又进入下一轮while循环,判断队列不为空,再将队中v1v_1v1出队,访问。

图8:BFS遍历过程5

图8:BFS遍历过程5

  在v1v_1v1顶点同样,以下标从小到大的顺序逐个检查其邻接顶点,找到v3v_3v3v5v_5v5(v0v_0v0已被访问则不再处理),将它们标记并入队。然后再进入下一轮while循环。

图9:BFS遍历过程6

图9:BFS遍历过程6

  将队中首位v3v_3v3出队进行访问,然后检查它的邻接顶点,发现v2v_2v2v4v_4v4符合条件,标记并入队。然后进入下一轮while循环。

图10:BFS遍历过程7

图10:BFS遍历过程7

  将队首v5v_5v5出队进行访问,检查其邻接顶点,其邻接顶点有v1v_1v1v4v_4v4,我们知道v1v_1v1已经被访问了,但是v4v_4v4却并没有被真正访问(输出),而是在队中,这样大家就能理解为什么要在入队时进行标记了,因为v4v_4v4已经在队中了,相当于我们心里已经有底了,它被访问只是迟早的事,没有必要再次入队了。此时v5v_5v5访问完之后就相当于我们(我们的分身)不会再继续探索了,他已经将他能所做的都做完了,则此轮循环没有顶点入队。进入下一轮while循环。

图11:BFS遍历过程8

图11:BFS遍历过程8

  后序过程则只剩将队中元素v2v_2v2v4v_4v4依次出队做访问,for循环中则因为没有符合条件的顶点了,所以没有入队,最终队列为空,while循环结束。同样回到BFSTraverse(G)中,继续检查下一个顶点v1v_1v1,发现它已经被访问了,则继续检查v2v_2v2,同理也被访问了,继续检查v3v_3v3v4v_4v4v5v_5v5均被访问了,则最终BFS遍历结束,遍历结果为:

v0→v1→v3→v5→v2→v4v_0 \rightarrow v_1 \rightarrow v_3 \rightarrow v_5 \rightarrow v_2 \rightarrow v_4v0v1v3v5v2v4

  我们将图3中的图转化为类树形结构,如下:

图12:图3对应类树形结构

图12:图3对应类树形结构

  可以看到,移去多余的边(v4,v5)(v_4,v_5)(v4,v5)后(多余即指在BFS中未被经过的边),它变成一棵树,而这棵树的层序遍历结果即为我们上面所求的BFS遍历结果。

  至此,我们完成了BFS的学习,有同学可能会想,DFS我们用了两种方式(递归、栈)实现,而BFS呢?事实上,BFS只能用队列实现,因为它的策略决定了它只能是“先进先出”的,只能用队列实现。

3、DFS与BFS总结

  我们通过上面的学习,已经掌握了DFS和BFS两种图的遍历策略,下面我们来总结一下它们的异同:

特点深度优先遍历(DFS)广度优先遍历(BFS)
遍历策略优先探索当前顶点的一个未被访问的邻接顶点优先探索当前顶点的所有未被访问的邻接顶点
数据结构栈(递归或显式栈)队列
访问顺序先深入后回溯先广后深
适用场景适合用于路径搜索、拓扑排序等适合用于最短路径搜索、层次遍历等

  通过上表,我们可以清晰地看到DFS和BFS的不同之处,同时也能理解它们各自的适用场景。选择合适的遍历策略,可以有效地解决不同类型的问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/946705.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

中转API为什么比官方更便宜?AI中转站成本揭秘

中转API为什么比官方更便宜?AI中转站成本揭秘 近两年,AI 大模型飞速发展,OpenAI、Anthropic、Google、Azure 等官方 API 成本依旧高企,大量个人和开发者无法承受长期高成本。但你会发现:市面上有不少 AI 中转站,…

Java 混合编程

Java内置了脚本语言引擎,支持直接对JavaScript语言的调用(直接调用是指直接在java源码中编写相应的脚本语言,然后通过引擎加载,直接使用),也支持Groovy和R语言,可通过 ScriptEngineManager manager = new Scrip…

Java 语法糖

for-each:语法简便,可避免数组越界,无法更新(更新是指把集合内的某一个对象替换成另一个,不是对象内部值的修改)和删除集合内对象,无法知道当前元素的位置索引 枚举:JDK5提出的,用enum进行声明,表示一组限定值…

JAVA RMI编程

RMI:远程方法调用,不同机器上的java进程通讯的时候,可以采用这种方法,基本流程,服务端到RMI上进行注册,客户端请求RMI,获取RMI上注册的服务,并存储到本地,客户端通过存储在本地地址,对服务端进行访问 与网络…

大资料毕业设计选题推荐-基于大数据的全球产品库存数据分析与可视化系统-大材料-Spark-Hadoop-Bigdata

大资料毕业设计选题推荐-基于大数据的全球产品库存数据分析与可视化系统-大材料-Spark-Hadoop-Bigdata2025-10-26 12:05 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: norma…

纸笔群群友命题乱做

进行日常纸笔练习/fendou感觉群友很厉害阿! W1 10/32 主题:Yajilin,我觉得很难的一类纸笔,该加训了。 基础规则:你需要染黑一些格子,并给出一条回路,经过所有白色格子,不经过线索格和涂黑格,线索格表示从这个…

本人对KMP如何匹配到所有结果的算法存在一些疑惑...

本人对KMP如何匹配到所有结果的算法存在一些疑惑...一秀尼... 算法 呀咯!!!!/*_ooOoo_o8888888o88" . "88(| -_- |)O\ = /O____/`---\____. \\| |// `./ \\||| : |||// \/ _||||| -:- |||…

字符与Java国际化编程

ASCII 美国信息交换标准代码 包含英语和西欧语言 GB2312 1980年提出,GBK 1995年提出 GB18030 2000年和2005年各有一个版本,主要表述汉字,包含的汉字越来越多 Unicode 字符集 ,目标包含所有的文字 实现方式主要是UT…

完整教程:京东100道GO面试题及参考答案(上)

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

Ubuntu 系统 /dev/sdb2(RAID 关联磁盘)挂载操作手册

一、磁盘核心信息梳理设备 容量 分区情况 状态 用途说明/dev/sda 446.63GiB /dev/sda1(512M,EFI 分区)、/dev/sda2(446.1G,Linux 分区) 已挂载(根目录 /) 系统盘,承载操作系统及核心文件/dev/sdb 1.75TiB /de…

2025 年 10 月厨房排烟、厨房排烟罩、厨房排烟系统厂家最新推荐,资质、案例、售后三维测评与选购指南

厨房排烟系统作为商用餐饮运营的核心基础设施,其性能直接关乎后厨环境安全与合规经营。据北京消防协会 2025 年行业报告显示,因安装不规范导致的排烟故障占厨房安全隐患总数的 62%,而中国质量协会数据表明,具备完善…

解决 Windows 下 Claude 通过 cmd/powershell 运行出错失去响应的问题

问题描述:Windows 下在终端中运行 Claude,如果尝试让它在磁盘中查找文件,很容易因为缺少 cgypath 导致 Claude 进程出错⎿ Error: Command failed: cygpath -u C:\Users\liutao\AppData\Local\Temp/usr/bin/bash: …

# Ubuntu 根目录空间扩展操作手册(基于 RAID 关联磁盘 /dev/sdb2)

核心结论 直接将 /dev/sdb2 挂载至根目录 / 风险极高,易导致系统崩溃,优先采用“LVM 逻辑卷扩容”(推荐)或“绑定挂载”方案,安全扩展根目录可用空间。 一、直接挂载根目录的风险警示根目录 / 包含 /etc(系统配置…

013的加密世界权威指南_第二部分

013的加密世界权威指南_第二部分前言 本文档是“013加密世界权威指南”系列的第二部分,聚焦于加密资产交易与去中心化金融(DeFi)的实践入门。内容涵盖顶级中心化交易所(CEX)的深度对比、交易费用机制的解析、去中心化…

Perplexity Comet AI浏览器「等待网络链接」解决方案

5步解决Perplexity Comet AI浏览器「等待网络链接」💻✨ 宝子们!是不是被Perplexity Comet AI浏览器的「等待网络链接」弹窗搞心态了?😭 试了N种方法都没用?别慌!亲测有效的5步解决方案来啦,跟着做秒搞定👇…

Redis 持久化 内存模型 - 指南

Redis 持久化 & 内存模型 - 指南pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monac…

25.10.25随笔NOIP模拟赛总结

考试 题好像很简单,优势在我(?)拿到题后一眼看出 T1,然后过到 T2,感觉是拆位啥的,继续看。T3 感觉是二分然后 dp 判断,然后 dp 感觉比较好做,T4 第一个想法就是先对每个点分类,然后去处理特殊的点,考虑一个…

新地球

是一种蓝色的哀愁 我醒在荒凉的地球 纽约在海底的角落 阳光把大地变沙漠 变了味道 人们才开始哀悼 讽刺的是空气很好 烟囱在 水里 无声 飘摇 没了情调 人们为三餐奔跑 一切的爱 恨 都在自寻烦恼 新地球 他们这么叫 脸上…

师生健康信息管理高效的系统|基于SpringBoot和Vue的师生健康信息管理系统(源码+数据库+文档)

师生健康信息管理高效的系统|基于SpringBoot和Vue的师生健康信息管理系统(源码+数据库+文档)2025-10-26 11:41 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !import…

详细介绍:在不同开发语言与场景下设计模式的使用

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …