=================================================
废柴日记5:迟到的『构造最小生成树算法』· 其二
=================================================
=================================================
日期:2021年9月16日 天气:多云转雨 已感冒┭┮﹏┭┮
=================================================
其实,我每天都还好。
在迟到的『构造最小生成树算法』①发布之后,博主发现,在博主所经常使用的两本数据结构书上,对于有向图的「连通」解释的很少,两本书都只是介绍了有向图中的「强连通图」与「强连通分量」。
本着「自己挖坑自己埋,砂锅打破问到底」的理念,博主去稍微了解了一下在有向图中的「连通」,并得到了如下信息:
- 强连通:在有向图G=(V , E)中,对于V中的两个不同顶点X、Y,存在X→Y与Y→X的路径,则我们称顶点X,Y是强连通的。
- 弱连通:在有向图G=(V , E)中,对于V中的两个不同顶点X、Y,存在X→Y或Y→X的路径,则我们称顶点X,Y是弱连通的。
- 强连通图:在有向图G=(V , E)中,对于V中的所有的不同顶点X、Y,都存在X→Y与Y→X的路径,则我们称有向图G为强连通图。
- 弱连通图:对于有向图G=(V , E),我们将图G中的所有的有向边全部替换成无向边,可以得到无向图G'。若无向图G'是连通图,则我们称有向图G为弱连通图。
这篇博客本应是2021年9月15日发布,但是博主昨天晚上发现有作业没写,补完作业的时候已经22:30了,宿舍晚上22:00断电,我又忘记给电脑开启省电模式。
于是就在2021年9月16日凌晨00:10分,在我祈求了无数遍不要关机之后,我的电脑还是没能撑住,电量过低自动关机了。
虽然写的东西都保存了,但是博主对于断更这件事表示深深的愧疚。
『感情用事是失败者的生理缺陷,晚上十点断电也是。』
但这也给了博主更多的时间去学习算法与完善自己对『构造最小生成树算法』的认知。
在博主再一次查阅资料之后,博主发现在之前的迟到的『构造最小生成树算法』①中博主有很多地方的用词很不严谨,这可能会导致部分人会对图产生一些误解,还有一些关键词的定义并未给出。
于是博主将第一篇回炉重造,进行了一番修改并重新发布出来,具体的修改内容博主已经写成Blink发布。
接下来废话不多说,进入正题。
前方高燃,闲人回避。
Ⅲ.补充知识(点与点的邻接)
- 无向图的「邻接」:对于无向图 G = ( V , E ),如果 边(v , v') 属于集合 E ,则称顶点 v 和 v' 互为邻接点(Adjacent),即 v,v' 相邻接。
- 有向图的「邻接」:对于有向图 G = ( V , E ),如果 弧< v,v' >属于集合E,则称顶点 v 邻接到顶点 v' ,顶点 v' 邻接自顶点 v 。
「邻接」对于有向图与无向图来说有着不同的解释,大家千万不要记混。
我们可以理解成:
- 在无向图中,每个顶点都是十分善良与友好的。如果顶点 v 将顶点 v' 视为朋友(顶点 v 与顶点 v' 之间存在边),那么顶点 v' 也会视顶点 v 为朋友。
- 而在有向图之中,每个顶点看上去很友好,实则都在暗地里做手脚。如果顶点 v 将顶点 v' 视为朋友(顶点 v 与顶点 v' 之间存在弧< v,v' >),顶点 v' 并不一定会视顶点 v 为朋友。
如果此时顶点 v 与顶点 v' 之间又存在弧< v',v >,则说明两者是互相邻接的,即双方都视对方为好友。
「邻接」这一概念在下面将会频繁的出现,需要大家熟练的分辨出两种「邻接」的不同。
坐稳扶好,顶点要开始「邻接」了。
Ⅳ.前置结构(图的存储结构)
结合之前所讲的理论知识,我们可以发现,图的结构十分复杂,任意的两个顶点之间都可能存在着联系。因此,如何对图实现物理存储是一个难题。
图的存储结构共有5种:邻接矩阵、邻接表、十字链表、邻接多重表与边集数组。
其中红色字体标注的邻接矩阵与边集数组采用的是顺序存储结构,而蓝色字体标注的邻接表、十字链表、邻接多重表采用的是链式存储结构。
大多数的数据结构课本主要讲解的是前三种存储结构。
接下来的知识讲解与代码实现将着重于邻接矩阵与邻接表,其余的三个存储结构博主在这里就不过多介绍了,有兴趣的话可以在网络上搜索相关信息。
(这并不是因为博主实在太菜了以至于讲不出来所以只能随便找个理由敷衍过去的,不是的!)
①邻接矩阵
如果学过线性代数的话,大家应该理解什么是矩阵。
由 m × n 个数aij排成的m行n列的数表称为m行n列的矩阵,简称m × n矩阵。记作:
这m×n 个数称为矩阵A的元素,简称为元,数aij位于矩阵A的第i行第j列,称为矩阵A的(i,j)元,以数 aij为(i,j)元的矩阵可记为(aij)或(aij)m × n,m×n矩阵A也记作Amn。
邻接矩阵的实质可以理解为使用两个数组来存储一张图中所有的信息。
为了方便大家理解代码部分,我们先举个例子(以无向图为例)。
举例说明
现有一张带权无向图H:
从图H中我们可以得到的信息:
- 图H的顶点集合为V={1,2,3,4,5};
- 图H的边集合为E={ (1,2),(1,5),(2,3),(2,4),(3,4),(4,5) };
接下来我们需要借助邻接矩阵将图H从二维空间移植到代码空间。
- 第一步:使用一维数组vexs存储顶点编号集合V,存储顺序为顶点编号从小到大的顺序。
- 第二步:初始化用于存储边集合E的二维数组(矩阵)arc。
二维数组arc的取值含义:
根据上述规则,我们在代码空间重构图H,通过第一步把5个顶点移植到了代码空间,此时点与点之间没有边相连(没有邻接点存在),所以初始的二维数组arc是这样的:
- 第三步:录入边集合E。
以边(1,2)举例说明。在无向图之中,顶点1邻接到顶点2,顶点2邻接到顶点1,所以
arc[1][2]=arc[2][1]=3;
此时二维数组arc变成这样:
剩下的边依次录入,最后形成的二维数组arc为:
到此为止,我们已经成功地把二维空间的图H通过邻接矩阵的存储结构存储在我们的代码空间之中。
『你发现了没有?』
『依据无向图的「邻接」最后形成的二维数组arc一定会是一个「对称矩阵」』
『但如果是依据有向图的「邻接」最后形成的二维数组arc不一定是一个「对称矩阵」』
『这也从侧面表明:』
『如果每个人都向别人敞开自己的心扉,人与人坦诚相待』
『最后得到的一定会是双倍的快乐(矩阵和)』
你没有发现,你只关心你自己😀
接下来是代码实现部分,可以结合上述样例与代码注释理解。
结构定义
/** 邻接矩阵的结构体定义代码 **/
typedef int VertexType; /** VertexType为顶点的数据类型,我们在第一篇博客中所展示的图中,所有的顶点的数据类型为int。 **/
typedef int EdgeType; /** EdgeType为边上权值的数据类型,我们接下来所研究的最小生成树是基于带权图的。 **/
#define MAXVEX 100 /** 顶点的最大数量,这里也可以看出邻接矩阵的局限性:顶点数量过多的时候无法处理。 **/
#define INF 0x3f3f3f3f /** 用0x3f3f3f3f代表∞,可以是其他数字,但推荐是这个。 **/
typedef struct
{
VertexType vexs[MAXVEX]; /** vexs数组存储图G所有的顶点,充当集合V的作用,下标从0开始。 vexs[i+1]=X => 图G的第i个顶点为X。 **/
EdgeType arc[MAXVEX][MAXVEX]; /** arc数组存储图G中所有的边,充当集合E的作用。 **/
int numNodes,numEdges; /** numNodes代表当前图G中的顶点数量,numEdges代表当前图G中的边的数量。 **/
}MGraph;
邻接矩阵创建代码实现
/** 建立无向图的邻接矩阵 **/
void CreateUMGraph(MGraph *G)
{
int i,j,x,y,w;
printf("请输入无向图G的顶点数与边数:\n");
scanf("%d%d",&G->numNodes,&G->numEdges); /** 读入顶点数与边数 **/
for(i=0;i<G->numNodes;i++) /** 根据顶点信息,建立顶点表 **/
scanf("%d",&G->vexs[i]);
for(i=0;i<G->numNodes;i++)
for(j=0;j<G->numNodes;j++)
G->arc[i][j]=INF; /** 邻接矩阵初始化**/
for(i=0;i<G->numEdges;i++) /** 根据读入的numEdges,建立邻接矩阵**/
{
printf("请输入(vi,vj)上的下标i,下标j与权值w:\n");
scanf("%d%d%d",&x,&y,&w); /** 读入边(vi,vj)上的权值w **/
G->arc[x][y]=w;
G->arc[y][x]=G->arc[x][y]; /** 无向图的邻接矩阵特点:对称性,即无向图的邻接矩阵是一个对称矩阵(n阶矩阵的元素满足aij==aji)**/
/** 如果是有向图的邻接矩阵建立的代码实现,只需要把上面一行删去即可**/
}
}
邻接矩阵的创建的时间复杂度:
对于具有n个顶点,m条边的图的邻接矩阵的创建:。
②邻接表
在对数据结构的学习之中,对于每一个新的数据结构,老师会为我们讲解一个顺序存储结构的形式与一个链式存储结构的形式。接下来所要说的邻接表就是一种链式存储结构。(博主不是很喜欢链表,链表的操作难度要比数组操作难度高很多,很具有挑战性)
在邻接表中我们使用单链表数组adjList同时存储顶点集合V与边集合E。
- 每个单链表的初始长度为1,只有一个结点。结点处存储着顶点编号。
- 当顶点A与顶点B之间存在边/弧时(也就是说有条件可以说明顶点A邻接到顶点B),我们就在存储顶点A的单链表的后面添加一个新的结点,这个结点存储着顶点B的编号和<A,B> / (A,B)的权值。
举个例子(以有向图为例)。
举例说明
现有一张带权有向图H,我们的任务就是使用邻接表存储结构来存储图H。
- 第零步:将图H的所有信息梳理一下。
从图H中我们可以得到的信息:
- 图H的顶点集合为V={1,2,3,4,5};
- 图H的边集合为E={ <2,1>,<2,3>,<1,5>,<4,5>,<4,2>,<3,4>};
- 第一步:将顶点集合V存入单链表数组adjList中每一个单链表的第一个结点中。结果如图所示。
- 第二步:将边集合V存入单链表数组adjList。
我们以添加弧<2,1>来举个例子。
首先根据有向图的「邻接」定义,我们可以从弧<2,1>中得知:
顶点2邻接到顶点1,顶点1邻接自顶点2。
邻接表的建立所需要使用的是这条信息的前半句:顶点2邻接到顶点1。此时我们在以顶点2作为第一个结点的单链表后添加一个新结点,结点中存储顶点编号1与弧<2,1>的权值6,代表顶点2与顶点1之间有一条弧,方向为顶点2→顶点1,弧的权值为6。
随后依次录入剩下的5条边,最后结果如图所示。
到这里,可能会有人产生一些疑问,我来回答一下一些最有可能出现的问题:
-
问:"博主博主,你看顶点2同时邻接了顶点1和顶点3,那这两个结点谁在前谁在后啊?"
答:结点之间的逻辑顺序由你录入的顺序决定,先输入哪条边/弧就哪个在前面。当然你也可以制定规则,比如顶点编号小的排在前面之类的。
-
问:"博主博主,你说邻接表用的是『顶点2邻接到顶点1,顶点1邻接自顶点2。』中的前半句,那后半句呢?"
答:额,这个本来没打算讲的,这里就稍微提一下。需要用到『顶点2邻接到顶点1,顶点1邻接自顶点2。』中的后半句的是邻接表的双胞胎兄弟——逆邻接表。邻接表+逆邻接表就变成了图的存储结构中的十字链表与邻接多重表了。
-
问:"博主博主,你只举了有向图的例子,那无向图呢?"
答:无向图的每一条边,其实是可以看成由两个权值相同、方向相反的弧拼接而成。比如边(1,2)就是由弧<1,2>和弧<2,1>组成。这个时候你再看无向图,它就变成了一张有向图,然后按照有向图的处理方式处理就好了。
接下来是代码实现部分,可以结合上述样例与代码注释理解。
结构定义
typedef struct EdgeNode /** 边表结点 **/
{
int adjvex; /** 邻接点的顶点下标 **/
EdgeType info; /** 边的权值 **/
struct EdgeNode *next; /** 链域,用于指向下一个邻接点**/
}EdgeNode;
typedef struct VertexNode /** 顶点表结点 **/
{
VertexType data; /** 顶点域,存储顶点的信息 **/
EdgeNode *firstedge; /** 指向该顶点的边表的首地址 **/
}VertexNode,AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numNodes,numEdges; /** numNodes代表当前图G中的顶点数量,numEdges代表当前图G中的边的数量。 **/
}GraphAdjList;
邻接表创建代码实现
void CreateUALGraph(GraphAdjList *G)
{
int i,j,x,y,w;
EdgeNode *e;
printf("请输入无向图G的顶点数与边数:\n");
scanf("%d%d",&G->numNodes,&G->numEdges); /** 读入顶点数与边数 **/
for(i=0;i<G->numNodes;i++)
{
scanf("%d",&G->adjList[i].data); /** 读入顶点信息 **/
G->adjList[i].firstedge=NULL; /** 将该顶点的边表置为空 **/
}
for(i=0;i<G->numEdges;i++) /** 建立边表 **/
{
printf("请输入(vi,vj)上的下标i,下标j与权值w:\n");
scanf("%d%d%d",&x,&y,&w); /** 读入边(vi,vj)上的权值w **/
e=(EdgeNode *)malloc(sizeof(EdgeNode)); /** 申请结点空间 **/
e->adjvex=y; /** 邻接序号为y **/
e->info=w;
e->next=G->adjList[x].firstedge; /** 将e的指针指向当前顶点所指的结点 **/
G->adjList[x].firstedge=e; /** 让顶点的指针指向e **/
/**若为有向图的邻接表建立,则不需要以下5行代码**/
e=(EdgeNode *)malloc(sizeof(EdgeNode)); /** 申请结点空间 **/
e->adjvex=x; /** 邻接序号为x **/
e->info=w;
e->next=G->adjList[y].firstedge; /** 将e的指针指向当前顶点所指的结点 **/
G->adjList[y].firstedge=e; /** 让顶点的指针指向e **/
}
}
邻接表的创建的时间复杂度:
对于具有n个顶点,m条边的图的邻接矩阵的创建:。