数据结构学习 --6 图
数据结构学习 --1 绪论
数据结构学习 --2 线性表
数据结构学习 --3 栈,队列和数组
数据结构学习 --4 串
数据结构学习 --5 树和二叉树
数据结构学习 --6 图
数据结构学习 --7 查找
数据结构学习 --8 排序
本人学习记录使用 希望对大家帮助 不当之处希望大家帮忙纠正
数据结构学习 --6 图
文章目录
数据结构学习 --6 图
6.1 图的基本概念
6.1.1 图的定义
基本概念及术语
- 有向图
- 无向图
- 简单图、多重图
- 完全图(也称简单完全图)
- 子图
- 连同、连通图和连同分量
- 强连通图、强连通分量
- 生成树、生成深林
- 顶点的度、入度和出度
- 边的权和网
- 稠密图、稀疏图
- 路径、路径长度和回路
- 简单路径、简单回路
- 距离
- 有向树
6.2 图的存储及基本操作
图的存储必须要完整、准确地反应顶点集合和边集的信息。根据不同的图的结构和算法,采取不同的存储方式对程序的效率产生相当大的影响,因此所选的存储结构应适应合适于带求解的问题
6.2.1 邻接矩阵法
所谓邻接矩阵存储,是指用一个一维数组存储图中顶点的信息,用一个二维数组存储图中边的信息(即各顶点之间的邻接关系),存储顶点之间邻接关系的二维数组称为邻接矩阵。
#define MaxVertexNum 100 //顶点数目的最大值
typedef char VertexType; //顶点的数据类型
typedef intEdgeType; //带权图中边上权值的数据类型
typedef struct{
VertexType Vex[MaxVertexNum];//顶点表
EdgeType Edge[MaxVertexNum][MaxVertexNum];//邻接矩阵,边表
int vexnum,arcnum; //图的当前顶点数和弧数
}MGraph;
图的邻接矩阵存储表示法具有以下特点:
-
无向图的邻接矩阵一定是一个对称矩阵(并且唯一)。因此,在实际存储邻接矩阵时只需存储上(或下)三角矩阵的元素。
-
对于无向图,邻接矩阵的第i行 (或第i列) 非零元素(或非元素)的个数正好是顶点i的度TD(v)。
-
对于有向图,邻接矩阵的第i行非零元素(或非元素的个数正好是顶点i的出度OD(v):第i列非元素(或非元)的个数正好是顶点i入度D(v)。
-
用邻接矩阵存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大。
-
稠密图适合使用邻接阵的存储示。
6.2.2 邻接表法
当一个图为稀疏图时,使用邻接矩阵法显然要浪费大量的存储空间,而图的邻接表法结合了顺序存储和链式存储方法,大大减少了这种不必要的浪费。
所谓邻接表,是指对图G中的每个顶点 建立一个单链表,第i个单链表中的结点表示依附于顶点 的边(对于有图则是以顶点 为尾的,这个单就称为顶点 (对于有向图则称为出边表)。边表的头指针和顶点的数据息采用序(称为项点),所以在接表中存在两种结点:顶点表结点和边表结点
6.2.3 十字链表
十字链表是有向图的一种链式存储结构。在十字链表中,对应于有向图中的每条弧有一个结点,对应于每个顶点也有一个结点。这些结点的结构如下图所示。
弧结点中有5个域:tailvex和 headvex 两个域分别指示弧尾和弧头这两个顶点的编号:hlink域指向弧头相同的下一个弧结点:tlink域指向弧尾相同的下一个弧结点:info域存放该弧的相关信息。这样,弧头相同的弧就在同一个链表上,弧尾相同的弧也在同一个链表上。顶点结点中有3个域:data 域存放该顶点的数据信息,如顶点名称;firstin 域指向以该顶点为弧头的第一个弧结点;firstout 域指向以该顶点为弧尾的第一个弧结点。
6.2.4 邻接多重表
邻接多重表是无向图的另一种链式存储结构。在邻接表中,容易求得顶点和边的各种信息,但在邻接表中求两个顶点之间是否存在边而对边执行删除等操作时,需要分别在两个顶点的边表中遍历,效率较低
与十字链表类似,在邻接多重表中,每条边用一个结点表示,其结构如下所示
在邻接多重表中,所有依附于同一顶点的边串联在同一链表中,由于每条边依附于两个顶点因此每个边结点同时链接在两个链表中。对无向图而言,其邻接多重表和邻接表的差别仅在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。
6.2.5 图的基本操作
图的基本操作是独立于图的存储结构的。而对于不同的存储方式,操作算法的具体实现会有着不同的性能。
在设计具体算法的实现时,应考虑采用何种存储方式的算法效率会更高
图的基本操作主要包括(仅抽象地虑,故忽略各变的类型):
Adjacent(G,x,y): 判断图G是否存在边<x>或y)。
Neighbors(G,x):列出图 G中与结点x邻接的边
InsertVertex(G,x):在图G中插入顶点X。
DeleteVertex(G,x): 从图 G中删除顶点X。
AddEdge(G,x,y):若无向边(xy)或有向边<x不存在,则向图G 中添加该边。
RemoveEdge(G,x,y):若无向边()或有向边<x存在,则从图G中删除该边。
FirstNeighbor(G,x):求图G中顶点x 的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回1。
NextNeighbor(G,x,y):假设图 G中顶点y 是顶点x 的一个邻接点,返回除y外顶点x的下一个邻接点的顶点号,若y是x的后一个邻接点,则返回-1。
·Get_edge_value(G,x,y);获取图G中边(y)或x,>对应的权值。
Set_edge_value(G,x,y,v):设置图G中边(y)或<x,对应的权值为v。
6.3 图的遍历
图的遍历是指从图中的某一顶点出发,按照某种搜索方法沿着图中的边对图中的所有顶点访问一次且仅访问一次。
注意到树是一种特殊的图,所以树的遍历实际上也可视为一种特殊的图的遍历。
图的遍历算法是求解图的连通性问题、拓扑排序和求关键路径等算法的基础。图的遍历比树的遍历要复杂得多,因为图的任意一个顶点都可能和其金的顶点相邻接,所以在访问某个顶点后,可能沿着某条路径搜索又回到该顶点上。
为避免同一顶点被访问多次,在遍历图的过程中,必须记下每个已访问过的顶点,为此可以设一个辅助数组 visited门来标记顶点是否被访问过。
图的遍历算法主要有两种:广度优先搜索和深度优先搜索
6.3.1广度优先搜索
广度优先搜索(Breadth-First-Search,BFS)类似于二叉树的层序遍历算法。基本思想是:首先访问起始顶点v,接着由v出发,依次访问v的各个未访问过的邻接顶点 w1,w2,…wi,然后依次访问 w1,w2,…wi的所有未被访问过的邻接顶点;再从这些访问过的顶点出发,访问它们所有未被访问过的邻接顶点,直至图中所有顶点都被访问过为止。
若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作为始点,重复上述过程,直至图中所有顶点都被访问到为止。Diikstra单源最短路径算法和Prim最小生成树算法也应用了类似的思想。
换句话说,广度优先搜索遍历图的过程是以v为起始点,由近至远依次访问和有路径相通且路径长度为1,2,.··的顶点。广度优先搜索是一种分层的查找过程,每向前走一步可能访问一批项点,不像深度优先搜索那样有往回退的情况,因此它不是一个递归的算法。为了实现逐层的访问,算法必须借助一个辅助队列,以记忆正在访问的顶点的下一层顶点。
bool visited(MAX VERTEX NUM]://访问标记数组
void BFSTraverse(Graph G){ //对图G进行广度优先遍历
for(i=0;i<G.vexnum;++i)
visited[i]=FALSE;//访问标记数组初始化
InitQueue(Q); //初始化辅助队列Q
for(i=0;i<G.vexnum;++i) //从0号顶点开始遍历
if(!visited[i]) //对每个连通分量调用一次 BES
BFS(G,i); //v1;未访间过,从v1开始BES
}
void BFS(Graph G,int v){ //从顶点v出发,广度优先遍历图G
visit(v); //访问初始项点v
visited[v]=TRUE; //对v做已访问标记
Enqueue(Q,v); //顶点v入队列Q
while(!isEmpty(Q))(
DeQueue(O.v) //顶点v出队列
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)) //检测v所有邻接点
if(!visited[w]){//w为v的尚未访问的邻接顶点
visit(w); //访问顶点w
visited[w]=TRUE; //对w做已访问标记
EnQueue(Q,w); //顶点w 入队列
}
}
}
-
BFS 算法的性能分析
无论是邻接表还是邻接矩阵的存储方式,BFS算法都需要借助一个辅助队列Q,n个顶点均需入队一次,在最坏的情况下,空间复杂度为 O(V)。
采用邻接表存储方式时,每个顶点均需搜索一次(或入队一次),故时间复杂度为 O(V),在搜索任意一个顶点的邻接点时,每条边至少访问一次,故时间复杂度为 O(E),算法总的时间复杂度为 O(M+ E)。采用邻接矩阵存储方式时,查找每个顶点的邻接点所需的时间为 O(V),故算法总的时间复杂度为 O(V²)。 -
BFS 算法求解单源最短路径问题
若图G=(V,E)为非带权图,定义从顶点u到顶点v的最短路径d(u,v)为从u到v的任何路径中最少的边数;若从u到v没有通路,则d(u,v)=∞
使用 BFS,我们可以求解一个满足上述定义的非带权图的单源最短路径问题,这是由广度优先搜索总是按照距离由近到远来遍历图中每个顶点的性质决定的。
BFS算法求解单源最短路径问题的算法如下:
void BFS MIN Distance(Graph G,int u){
//d[i]表示从口到i结点的最短路径
for(i=0;i<G.vexnum;++i)
d[i]=∞;//初始化路径长度
visited[u]=TRUE;
d[u]=0;
EnQueue(Q.u);
while(!isEmpty(Q)){//BES算法主过程
DeQueue(Q,u) //队头元素u出队
for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w))
if(!visited[w]){ //w为u的尚来访问的邻接顶点
visited(w]=TRUE; //设已访问标记
d[w]=d[u]+1; //路径长度加1
EnQueue(Qrw); /顶点w入队
}
}
- 广度优先生成树
在广度遍历的过程中,我们可以得到一颗遍历树,称为广度优先生成树
6.3.2 深度优先搜索
与广度优先搜索不同,深度优先搜索 (Depth-First-Scarch,DFS)类似于树的先序遍历。
如其名称中所暗含的意思一样,这种搜索算法所遵循的搜索策略是尽可能“深”地搜索一个图.
它的基本思想如下:首先访问图中某一起始顶点 v,然后由出发,访问与邻接且未被访问的任意一个顶点 w1,再访问与 w 邻接且未被访问的任意一个顶点 w2···重复上述过程。
当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止。
一般情况下,其递归形式的算法十分简洁,算法过程如下:
bool visited[MAX VERTEX NUM]; //访问标记数组
void DFSTraverse(Graph G){//对图G进行深度优先遍历
for(v=O;v<G.vexnum;++v)
visited[v]=FALSE; //初始化已访问标记数组
for(v=0;v<G.vexnum;++v) //本代码中是从v=0开始遍历
if(!visited[v])
DFS(G,V);
}
void DFS(Graph G,int v){//从顶点v出发,深度优先遍历图G
visit(v);//访问顶点v
visited[v]=TRUE; //设已访问标记
for(w=FirstNeiqhbor(G,v);w>=0;w=NextNeighbor(G,v,w))
if(!visited[w])//w为v的尚未访问的邻接顶点
DFS(G,w);
}
- DFS算法的分析
DFS算法是一个递归算法,需要借助一个递归工作栈,故其空间复杂度为 O(v)。遍历图的过程实质上是对每个顶点查找其邻接点的过程,其耗费的时间取决于所用的存储结勾。以邻接矩阵表示时,查找每个顶点的邻接点所需的时间为 O(v),故总的时间复杂度为 O(v²)以邻接表表示时,查找所有顶点的邻接点所需的时间为 O(E),访问顶点所需的时间为 O(v),此时,总的时间复杂度为 O(v+E)。
2,深度优先的生成树和生成森林
与广度优先搜索一样,深度优先搜索也会产生一棵深度优先生成树。当然,这是有条件的,即对连通图调用 DFS 才能产生深度优先生成树,否则产生的将是深度优先生成森林,如图 6.13所示。与 BFS类似,基于邻接表存储的深度优先生成树是不唯一的。
6.3.3 图的遍历与图的连通性
图的遍历算法可以用来判断图的连通性
对于无向图来说,若无向图是连通的,则从任意一个结点出发,仅需一次遍历就能够访问图中的所有顶点;
若无向图是非连通的,则从某一个顶点出发,一次遍历只能访问到该顶点所在连通分的所有顶点,而对于图中其他连通分量的顶点,则无法通过这次遍历访问。对于有向图来说,若从初始点到图中的每个顶点都有路径,则能够访问到图中的所有顶点,否则不能访问到所有顶点。
6.4 图的应用
6.4.1 最小生成树
一个连通图的生成树包含图的所有顶点,并且只含尽可能少的边。对于生成树来说,若砍去它的一条边,则会使生成树变成非连通图;若给他增加一条边,则会形成图中的一条回路。
- Prim 算法
Prim(普里姆)算法的执行非常类似于寻找图的最短路径的 Dijkstra算法(见下一节Prim算法构造最小生成树的过程如图6.15所示。始时从图中任取一顶点(如顶点1)加入树 T,此时树中只含有一个顶点,之后选择一个与当前T中顶点集合距离最近的顶点,并将该顶点和相应的边加入 7,每次操作后T中的顶点数和边数都增 1。以此类推,直至图中所有的顶点都并入 T,得到的T就是最小生成树。此时 T中必然有n-1条边。
- Kruskal 算法
与Prim 从顶点开始扩展最小生成树不同,Kruskal (克鲁斯卡尔)算法是一种按权值的递增次序选择合适的边来构造最小生成树的方法
6.4.2 最短路径
6.3 节所述的广度优先搜索查找最短路径只是对无权图而言的。当图是带权图时,把从一个顶点到图中其余任意一个顶点的一条路径(可能不止一条)所经过边上的权值之和,定义为该路径的带权路径长度,把带权路径长度最短的那条路径称为最短路径。
求解最短路径的算法通常都依赖于一种性质,即两点之间的最短路径也包含了路径上其他顶点间的最短路径。带权有向图G的最短路径问题一般可分为两类:
一是单源最短路径,即求图中某一顶点到其他各顶点的最短路径,可通过经典的 Dijkstra (迪杰斯特拉)算法求解,
二是求每对顶点间的最短路径,可通过 Floyd (弗洛伊德)算法来求解
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通