当前位置: 首页 > news >正文

【数据结构】图论探秘:广度优先遍历(BFS)与生成树的构建艺术

广度优先遍历

  • 导读
  • 一、图的遍历
  • 二、广度优先搜索
    • 2.1 基本思想
    • 2.2 算法逻辑
      • 2.2.1 连通图的遍历
      • 2.2.2 非连通图的遍历
    • 2.3 算法分析
    • 2.4 广度优先生成树
  • 🌟 结语 | 探索不止,下期更精彩

广度优先遍历

导读

大家好,很高兴又和大家见面啦!!!

在前面的内容中,我们已经认识了图,学习了图的一些基本概念与核心术语以及4种图的存储结构:

  • 邻接矩阵:适合存储稠密图
  • 邻接表:适合存储稀疏图
  • 十字链表:适合存储有向图
  • 邻接多重表:适合存储无向图

并且我们还了解了图的一些基本操作:

  • Adjacent(G, x, y): 判断图G是否存在边<x, y>(x, y)
  • Neighbors(G, x): 列出图G中与结点x邻接的边
  • InsertVertex(G, x): 在图G中插入顶点x
  • DeleteVertex(G, x): 在图G中删除顶点x
  • AddEdge(G, x, y): 若无向边(x, y)或有向边<x, y>不存在,则向图G中添加改边
  • RemoveEdge(G, x, y): 若无向边(x, y)或有向边<x, y>存在,则从图G中删除该边
  • FirstNeighbors(G, x): 求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1
  • NextNeighbors(G, x, y): 假设图G中顶点 y 是顶点 x 的一个邻接点,返回除 y 外顶点 x 的下一个邻接点的顶点号,若 y 是 x 的最后一个邻接点,则返回-1
  • Get_edge_value(G, x, y): 获取图G中边(x, y)<x, y> 对应的权值
  • Set_edge_value(G, x, y, v): 设置图G中边(x, y)<x, y> 对应的权值

从今天开始,我们将会进入图的遍历操作的学习。下面我们就进入今天的内容;

一、图的遍历

图的遍历是指从图中的某一顶点出发,按照某种搜索方法沿着图中的边对图中的所有顶点访问一次,且仅访问一次。

在前面的内容中我们有说过,之前学习的数据结构:

  • 线性表:顺序表、链表、栈、队列……
  • 集合

都属于一种特殊图。因此这些数据结构的遍历都可以视为一种图的遍历。图的遍历算法是求解图的连通性问题、拓扑排序和求关键路径等算法的基础。

在这些数据结构中,树的遍历要比线性表或者集合的遍历要复杂,而图的遍历要比树的遍历更加复杂,这是因为两种数据结构的元素之间的关系之间存在差异:

  • 树形结构:元素之间满足一对多的关系
  • 图状结构:元素之间满足多对多的关系

由于这种多对多的关系,这使得图中的任意一个顶点都可能与其余顶点相邻接,所以在访问某个顶点后,可能沿着某条路径搜索又回到了该顶点。

为了避免同一顶点被多次访问,在遍历图的过程中,必须记下每个已访问过的顶点,为此可以设一个辅助数组visited[]来标记顶点是否被访问过。

在图中,遍历算法主要有两种:

  • 广度优先搜索
  • 深度优先搜索

今天我们将会介绍第一种遍历——广度优先搜索。

二、广度优先搜索

广度优先搜索(Breadth-First-Search, BFS)类似于树的层序遍历。

PS:对树的层序遍历感兴趣的朋友可以回顾【数据结构】C语言实现二叉树的基本操作——二叉树的层次遍历、求深度、求结点数……
在这篇博客中,有介绍二叉树中的层序遍历以及C语言的实现,这里我就不再展开赘述。

2.1 基本思想

BFS 的基本思想是:

  • 首先访问起始顶点v;
  • 接着从v出发,依次访问v的各个未被访问过的邻接顶点 w 1 , w 2 , … … , w i w_1, w_2, ……, w_i w1,w2,……,wi 的所有未被访问过的邻接顶点;
  • 再从这些访问过的顶点出发,访问它们所有未被访问过的邻接顶点;
  • 直至图中的所有顶点都被访问过为止;
  • 若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作为起始点,重复上述过程,直至图中所有顶点都被访问到为止。

换句话说,BFS 遍历图的过程就是以v为起点,由近至远依次访问和v有路径相通且路径长度为1, 2……的顶点。

从二叉树的角度来看,BFS 就是一种分层查找的过程,与起始点v路径相同的顶点位于同一层,每一次访问都会完成同一层全部顶点的访问。

二叉树与图

在上图中,各个顶点按路径长度的分层如下:

  • 顶点b, c 与起始点a的路径长度为1,因此b, c位于同一层
  • 顶点d, e与起始点a的路径长度为2,因此d, e位于同一层
  • 顶点f, g与起始点a的路径长度为3,因此f, g位于同一层

因此按照图的广度优先搜索进行遍历时,获得的遍历序列为: abcdefg

不过在图中,其遍历并不是唯一的,具体的遍历序列需要根据图的存储结构确定:

  • 邻接矩阵:图的遍历序列唯一
  • 邻接表、十字链表、邻接多重表:图的遍历序列不唯一

在这四种存储结构中,十字链表与邻接多重表的实现比较复杂,下面我们还是以简单的邻接矩阵和邻接表这两种存储结构来介绍BFS;

2.2 算法逻辑

2.2.1 连通图的遍历

因为广度优先遍历是一种层序遍历,参考二叉树的层序遍历,我们同样可以通过辅助队列来完成整个遍历的过程:

  • 队列中存放当前遍历层的顶点信息以及下一层的顶点信息;
  • 队头元素为当前需要进行访问的顶点
  • 当队头元素出队时,与该顶点相邻的且未被访问过的顶点入队
  • 当队列为空时,完成所有顶点的遍历

但是在图中,由于顶点之间可能存在相互邻接,那么为了正确判断当前顶点是否被访问,我们则需要通过一个额外的数组visited[]来判断当前邻接顶点是否被访问过。

整个逻辑可以用C语言代码表示为:

bool visited[MAX_VERTEX_NUM];					// 访问标记数组
Queue* q = NULL;								// 队列指针
// 广度优先搜索
void BFS(Graph* G, int i) {q = Create_Queue();							// 创建队列visit(i);									// 访问起始点visited[i] = true;							// 添加访问标记EnQueue(q, i);								// 遍历起点入队while (!Is_Empety) {						// 队列为空时,停止遍历ElemType x = DeQueue(q);				// 队首元素出队for (ElemType y = FirstNeighbors(G, x); y != -1; y = NextNeighbors(G, x, y)) {if (!visited[y]) {					// 当前顶点未被访问visit(y);						// 访问当前顶点visited[y] = true;				// 添加访问标记EnQueue(q, y);					// 当前顶点入队}}}
}

在整个遍历的过程中,我们需要注意的是邻接顶点的获取。不同的存储结构,其获取邻接顶点的方法也有所不同。但是,不管采用何种存储结构,其广度优先遍历的逻辑是不会发生改变:

  • 创建好一个空队列
  • 遍历起始点并进行标记
  • 起始点入队
  • 队首元素出队
  • 获取与队首顶点相邻的下一个顶点信息
    • 当该顶点被访问过,继续获取除该顶点以外的下一个顶点信息
    • 当该顶点未被访问过,访问该顶点并进行标记,将该顶点入队
  • 当队列为空时,完成连通图中所有顶点的访问

2.2.2 非连通图的遍历

当给定的图为非连通图时,如果我们只是进行简单的 BFS 是无法完成对非连通图中的所有连通分量的访问的,因此我们还需要通过 visited[] 数组来判断是否存在未被访问的顶点:

// 图的BFS
void BFSTraverse(Graph* G) {// 初始化标记数组for (int i = 0; i < MAX_VERTEX_NUM; i++) {visited[i] = false;}// 检查是否存在未被访问的顶点for (int i = 0; i < MAX_VERTEX_NUM; i++) {if (!visited[i]) {		// 当前顶点未被访问BFS(G, i);			// 通过BFS完成该连通分量的访问}}
}

在上述代码中,BFS 被调用的次数与连通分量的个数相同:

  • 当图G为连通图时,只会调用一次BFS 且起始点从下标为0的顶点开始
  • 当图G为非连通图时,每次调用 BFS 都会完成一个连通分量的遍历

在后续的内容中,我们将会完成图中各种基本操作的实现,这里就不再展开,大家记得关注哦!!!

2.3 算法分析

在整个算法中,其主要的时间消耗为:

  • 标记数组的初始化 —— O ( ∣ V ∣ ) O(|V|) O(V)
  • 标记数组的检查 —— O ( ∣ V ∣ ) O(|V|) O(V)
  • BFS的调用
    • 邻接表的实现中:
      • 顶点的入队 —— O ( ∣ V ∣ ) O(|V|) O(V)
      • 访问顶点 x 的所有邻边 —— O ( ∣ E ∣ ) O(|E|) O(E)
      • 总的时间复杂度为: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(V+E)
    • 邻接矩阵的实现中:
      • 顶点的入队 —— O ( ∣ V ∣ ) O(|V|) O(V)
      • 访问顶点x的所有邻边 —— O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)
      • 总的时间复杂度为: O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)

在前面的内容中我们有过详细的介绍,这里就不再重复赘述。

2.4 广度优先生成树

广度优先生成树 指的是通过 BFS 遍历图时,所得到的一个遍历树,该遍历树中保留了所有的顶点以及被正常访问的边。

这里需要理解的是何为正常访问的边,我们以下图为例:

a
b
c
d

在这个图中,各顶点与其邻接顶点的关系为:

  • 顶点a与顶点b、顶点c相邻
  • 顶点b与顶点a、顶点c、顶点d相邻
  • 顶点c与顶点a、顶点b、顶点d相邻

当我们从顶点a开始进行 BFS 时,我们在完成顶点a的访问后,此时的队列中只有顶点a:

队尾
a
队头

接下来会进一步访问其邻接顶点,与顶点a相邻的有两个顶点:

  • 顶点b:未访问
  • 顶点c:未访问

这里我们以访问顶点b为例,对应的边为: ( a , b ) (a, b) (a,b)。当访问完顶点b后,此时队列中只有顶点b:

队尾
b
队头

接下来我们会继续访问顶点a除顶点b以外的下一个顶点c,对应的边为: ( a , c ) (a, c) (a,c)。完成顶点c访问后,此时队列中存在顶点b与顶点c:

队尾
c
b
队头

第一轮对顶点a的邻接顶点访问结束,下面开始对顶点b的邻接顶点进行访问,此时与顶点b相邻的有3个顶点:

  • 顶点a:已访问
  • 顶点c:已访问
  • 顶点d:未访问

由此我们需要继续访问的是顶点d,对应的边为: ( b , d ) (b, d) (b,d)。当完成了对顶点d的访问后,此时的队列中存在的顶点为:顶点d和顶点c

队尾
d
c
队头

接下来不管是对顶点c还是顶点d的邻接点进行访问,都不存在未被访问的顶点,因此我们实际完成的访问边为: ( a , b ) − > ( a , c ) − > ( b , d ) (a, b)->(a, c)->(b, d) (a,b)>(a,c)>(b,d),其边与对应顶点所得到的生成树为:

a
b
c
d

在原图中,边 ( b , c ) 、 ( c , d ) (b, c)、(c, d) (b,c)(c,d) 在整个遍历的过程中未完成访问,因此在生成树中被去掉。

PS:
判断一条边是否被访问,是通过判断在遍历的过程中是否同时访问了该边的两个顶点。如:

  • 队首元素为顶点a,则当a完成出队操作后正常访问的顶点,其依附于该顶点与顶点a的边即为完成访问的边。

对于不同的存储结构而言,同一个图的广度优先生成树也存在区别:

  • 邻接矩阵:有且仅有唯一的广度优先生成树
  • 邻接表:有不唯一的广度优先生成树
  • 十字链表:有不唯一的广度优先生成树
  • 邻接多重表:有不唯一的广度优先生成树

导致这一差异的区别在于对边信息的存储方式的差异:

  • 邻接矩阵:边的信息通过矩阵存储,对应的矩阵唯一
  • 邻接表、十字链表、邻接多重表:边的信息通过链表进行存储,对应的边链表不唯一

因此,在探讨一张图所对应的广度优先生成树时,一定要弄清其具体的存储结构;

🌟 结语 | 探索不止,下期更精彩

广度优先搜索的内容我们就先介绍到这里,通过本文的学习,我们深入剖析了广度优先遍历(BFS)的核心逻辑:从分层遍历的思想到队列与visited数组的巧妙配合,从连通图的逐层扩散到非连通图的主动扫描,最终揭开广度优先生成树的奥秘。无论是邻接矩阵的唯一性,还是邻接表的灵活性,BFS都为我们展现了图遍历的分层之美。

📢 你的支持,是我持续创作的动力!
点赞❤️:如果这篇博客让你对BFS有了更清晰的理解,不妨点个赞支持一下吧~

收藏⭐️:将本文加入收藏夹,随时回顾算法精髓!

转发🚀:分享给更多朋友,一起踏上图论学习的征程!

关注🔔:蒙奇D索大,第一时间获取深度优先搜索(DFS)的硬核解析!

🔜 下期预告 | 深度优先搜索(DFS):一场“不撞南墙不回头”的探索之旅

  • 当BFS的“广撒网”遇到DFS的“一条路走到黑”,会碰撞出怎样的火花?

  • 递归与栈的博弈:DFS如何用回溯思想挖掘图的深层结构?

  • 生成树 vs 生成森林:DFS的遍历结果又有哪些独特性质?

  • 应用场景:拓扑排序、强连通分量、迷宫回溯……DFS的实战价值等你解锁!

🚀 敬请期待! 我们下期见~
💬 互动讨论:你对BFS还有哪些疑问?欢迎评论区留言,我们一起探讨!

相关文章:

  • 篇章二 数据结构——前置知识(二)
  • C++修炼:哈希表的模拟实现
  • 篇章一 数据结构——前置知识(一)
  • 数据结构之图结构
  • Mysql高版本(8.0及以后)Linux安装
  • leetcode hot100刷题日记——第一周没做好的题目总结
  • 深度图数据增强方案-随机增加ROI区域的深度
  • 机器学习--分类算法
  • vllm 2080TI ubuntu环境安装
  • 精选19道SQL面试题:覆盖查询、概念与常见陷阱
  • 论文阅读:PURPLE: Making a Large Language Model a Better SQL Writer
  • 【Stock】日本蜡烛图技术总结(1)——反转形态
  • 使用CentOS部署本地DeekSeek
  • React从基础入门到高级实战:React 核心技术 - 组件通信与 Props 深入
  • 只能上百度b站打不开其他网页
  • Linux之概述和安装vm虚拟机
  • JVM八股速查
  • RabbitMQ 可靠性保障:消息确认与持久化机制(二)
  • 篇章二 基础——包装类
  • SQL JOIN
  • 网站后台页面是什么/百度竞价代运营
  • 音乐 wordpress/郑州优化公司有哪些
  • 2019年云南建设银行招聘网站/网络营销的作用
  • 做网站怎么做/搜索引擎排名谷歌
  • 佛山市手机网站建设公司/云南seo简单整站优化
  • 网站垃圾代码检查工具/经典软文