寻找无向图的最小生成树
新手小白一枚,学习数据结构的途中想要留下自己的学习路径,在输入的同时做一点输出,巩固自己的学习成果,于是想要开启自己的博客之旅,作为我的第一篇博客肯定会是漏洞百出,如果能得到各位看官的帮助真的是感激不尽。希望我的编程技术和写博客的技术能在这样的尝试中有所提升,用小白的视角也许也能帮助到跟我有同样困惑的初学者也是说不定的呢。
ps:图片大部分都是从《大话数据结构》这本书上截下来的,代码也是在我看了大话之后写的
1. 图的存储方式
既然是寻找图的最小生成树,我们首先要了解图的存储方式
- 第一种是邻接矩阵(Adjacency Matrix)的存储方式:这个方法简单来说就是用一个一维数组存储顶点,一维数组的下标做为顶点命名的下标,再用一个二维数组存储对应的边的信息,若不表示权重的话,在无向图中可以直接用0,1表示两个顶点间是否有边,若表示权值的话,可以换成权值,把没有边相连的点对应的边表中的位置用极大值表示。
而对于有向图来说的话可以只考虑出度。
typedef struct Graph{
ElemType VertexNode[maxvex];
EdgeType EdgeNode[maxvex][maxvex];
int VexNum,EdgeNum;
}Graph;
- 另一种是邻接表的处理方式:本质上与前者如出一辙只是结构节点的结构有所变化,
这样就很直观了,顶点用一维数组表示,不过从图中看起来会像是二维数组,如果你的data下标与数组的下标一致的话,就不用大费周章了。firstedge存储的就是当前下标所邻接的第一条边(应该是在循环中根据头插法进入的最后一个),单独看一个顶点就可以明确的知道是以单链表的形式,顶点为链表的头节点,依次为邻接的边的下标(有向图同理)
typedef struct VertexNode{//顶点节点
ElemType data;
EdgeNode *firstedge;//第一个边节点
}VertexNode;
typedef struct EdgeNode{//边节点
ElemType data;
......//可以任意添加所需的信息例如打印权重
EdgeNode *next;
}EdgeNode;
2. prim算法
prim算法的本质不难理解,实质是在我们程序的进行过程中将图中的顶点分为两个部分,一个部分为已经加入生成树的部分,另一个部分为没有加入树的部分,我们要寻找的下一条边的顶点一个在第一部分,一个在第二部分。由此可知,我们需要一个数组可以表示顶点的状态即是否加入生成树,另一个数组存储备选的边的信息,对于有着相同终点的边,在我们不断循环的过程中是相互覆盖的。那么我们就是在不断地循环中将所有的顶点加入树,则完成。
算法流程:
- 定义保存顶点信息的数组Vex,保存边权值相关信息的数组EdgeCost;
- 首先将第一个元素加入树(随便哪个开始都可以),及Vex[0]=0,EdgeCost[0]=0;
- 然后对当前的顶点的所有相关联的边进行遍历,并存入对应的EdgeCost节点内,并将Vex中的节点都赋值为0,因为将这两个数组对应起来看的话,相同下标中存储的一个是边的权值一个是边的起点,对应的下标是终点,这对于我们的输出很重要;
- 接下来是对其他所有的顶点的遍历;
- 对于生成的EdgeCost数组进行遍历,寻找到最小的边,并记住他的节点下标(也就是在数组中的下标,表示的是边的中点下标);
- 找到这个边对应的起点,输出起点与终点,权值信息;
- 在数组EdgeCost中将终点对应的下标的节点值置为0,表示此节点已纳入生成树;
- 重新更新EdgeCost数组,遍历以新加入的点为起点的边,若其终点没有加入树,且遍历到的边权值更小,则更新这个节点中的权值信息,并将Vex数组中的对应节点的值置为此顶点的下标(表示这个边由这个顶点开始);
- 当所有节点遍历完成后结束;
void FindMintree_prim(MGraph G){
int i,j,k,min;
int Vex[G.VexNum],EdgeCost[G.VexNum];
Vex[0]=0;
EdgeCost[0]=0;
for(i=1;i<G.VexNum;i++){//第一个节点需要初始化
EdgeCost[i]=G.EdgeNode[0][i];//待选的边
Vex[i]=0;//所有对应的边的起点,在这里全是0
}
for (i=1;i<G.VeecNum;i++){//对其他的点进行遍历
min=INFINITY;//初始化为极大值例如65535
j=1;
k=0;//k中临时存储将要加入的节点
while(j<G.VexNum){//遍历当前的待选边数组
if(EdgeCost[j]!=0&&EdgeCost<min){//找出最小的边,获得其终点
min=EdgeCost[j];
k=j;
}
j++;
}
printf("(%d,%d)%d",Vex[k],k,EdgeCost[k]);//输出找的当前的最小边
EdgeCost[k]=0;//表示这个顶点已经进入树
for(j=1;j<G.VexNum;j++){//更新待选边
if(EdgeCost[j]!=0&&G.arc[k][j]<EdgeCost[j]){
EdgeCost[j]=G.arc[k][j];
Vex[j]=k;
}
}
}
}
3. Kruskal
相比而言,kruskal的算法比prim更好理解(至少我这么认为),其实质就是定义一个边集数组,对其按照权值由低到高排序,依次遍历,加入一条边之前,判断加入这条边之后会不会生成环,生成环则不加入,否则加入,直到遍历完全。(这一看就很简单)
首先我们定义一个边结点结构体
typedef struct Edge{
int begin ;
int end ;
int weight;
}Edge;
定义函数Findparent
int Findparent(int *parent,int f){
while (parent[f]>0){
f=parent[f];
}
return f;
}
算法思路
- 首先声明结构体数组Edges,并将传入的图的边的信息存储在其中,并排序(算法中省略);
- 定义一个parent数组,其中存储的是已经进入节点的点的关系,在后面课可以体会到,初始化;
- 开始对每一条边进行循环,对每一个边结构体中的begin与end元素调用Findparent函数,若返回值相同则表明生成环,此边不能进入,否则进入;
- 当一条可以加如生成树后,输出其信息,并在parent数组中将begin返回值下标的节点的值置为end返回的值,这种串联方式将这个树抽象成了一个单向链,有效的避免了我们定义边的时候采用起始点带来的方向问题;
void Findmintree_kruskal(MGraph G){
int i,n,m;
Edge Edgds[G.EdgeNum];
int parent[G.VexNum];
for(i=0;i<G.VexNum){
parent[i]=0;
}
for (i=0;i<G.EdgeNum;i++){
n=Findparent(parent,Edges[i].begin);
m=Findparent(parent,Edges[i].end);
if(n!=m){
parent[n]=m;
printf("(%d,%d) %d",Edges[i].begin,Edges[i].end,Edges[i].weight);
}
}
}
ecoli out