浅谈数据结构-最小生成树
一个连通图的生成树是一个极小的连通子图,它含有图中全部顶点,但只有足以构成一棵树的n-1条边。那么我们把构造连通网的最小代价生成树称为最小生成树。 找连通网的最小生成树,经典的有两种算法,普里姆算法和克鲁斯卡尔算法。
一、普利姆(Prim)算法
普利姆算法,图论中一种算法,可在加权连通图里搜索最小生成树。此算法搜索到的边子集所构成的树中,不但包括连通图里的所有顶点,且所有边的权值最小。
1、算法思想
从单一顶点开始,普利姆算法按照以下步骤逐步扩大树中所包含顶点的数目,直到遍及连通图的所有顶点。
- 输入:一个加权连通图,含有顶点V,边集合为E;
- 初始化:确定连通图的初始点,Vnew = {x},Enew = 0;
- 循环:直到Vnew = V
- 在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
- 将v加入集合Vnew中,将<u, v>边加入集合Enew中;
- 输出:使用集合Vnew和Enew来描述所得到的最小生成树。
2、算法分析
根据算法思想,整理下程序设计需要
- 创建一个图的存储结构(文章是邻接矩阵)
- 创建一个数组,保存图中的点(其实是在邻接矩阵中顶点表的坐标),其中全部初始化为0.
- 创建一个数组,保存边集合中的权重,保存顶点之间的权值。假如从D顶点开始建立树结构,这个数组就是表示D到各个顶点的距离。 权重为0.表示次顶点完成任务。
- 在权重数组中找出与初始顶点的权重最小的顶点,记录下的坐标。 并将权重数组中权重设为0.
3、图例解释
4、示例代码
//prime算法 void GraphData::MiniSpanTree_Prime(GraphArray *pArray) { int min,i,j,k; int nNodeIndex[MAXVEX]; //保存相关顶点坐标,1就是已经遍历访问的过结点 int nNodeWeight[MAXVEX]; //保存某个顶点到各个顶点的权值,为不为0和最大值表示遍历过了。 //两个数组的初始化 printf("开始初始化,当前顶点边的权值为:"); for(i = 0;i<pArray->numVertexes;i++) { nNodeIndex[i] = 0; nNodeWeight[i] = pArray->arg[0][i];//设定在矩阵中第一个顶点为初始点。 printf(" %c",nNodeWeight[i]); } //Prime算法思想 for (i = 1;i< pArray->numVertexes;i++) { min = INFINITY; //初始化权值为最大值; j = 1; k = 0; // 循环全部顶点,寻找与初始点边权值最小的顶点,记下权值和坐标 while(j < pArray->numVertexes) { //如果权值不为0,且权值小于min,为0表示本身 if (nNodeWeight[j] != 0&&nNodeWeight[j] < min) { min = nNodeWeight[j]; k = j; //保存上述顶点的坐标值 } j++; } printf("当前顶点边中权值最小边(%d,%d)\n",nNodeIndex[k] , k); //打印当前顶点边中权值最小 nNodeWeight[k] = 0; //将当前顶点的权值设置为0,表示此顶点已经完成任务 for (j = 1;j< pArray->numVertexes;j++) //循环所有顶点,查找与k顶点的最小边 { //若下标为k的顶点各边权值小于此前这些顶点未被加入的生成树权值 if (nNodeWeight[j] != 0&&pArray->arg[k][j] < nNodeWeight[j]) { nNodeWeight[j] = pArray->arg[k][j]; nNodeIndex[j] = k; //将下标为k的顶点存入adjvex } } //打印当前顶点状况 printf("坐标点数组为:"); for(j = 0;j< pArray->numVertexes;j++) { printf("%3d ",nNodeIndex[j]); } printf("\n"); printf("权重数组为:"); for(j = 0;j< pArray->numVertexes;j++) { printf("%3d ",nNodeWeight[j]); } printf("\n"); } }
5、程序分析
- 首先选取A作为初始顶点,从边的邻接矩阵中得知,最近的点是D,坐标是3,边表示为(0,3)
- 这时候权重数组中坐标为3的设为0.
- 在所有顶点中寻找到D的最小距离的顶点,从邻接矩阵得到是坐标为5,就是顶点F,边表示为(3,5)
- 将D中一行的权重与权重数组比较,将较小的值,保存其中。
- 在权重数组中寻找最小的且不为0的,发现时权重为6,坐标是5,就是之前确定的边(3,5),已F开始需找到F的最小边。如此循环。
- 从坐标数组中我们得知边为(nNodeInde[i],i),所以为(0,1)(4,2)(0,3)(1,4)(3,5)(4,6)。
二、克鲁斯卡尔(Kruskal)算法
普利姆算法是从某一个顶点开始,逐步找各个顶点上最小权值的边构建来最小生成树。同样的思路,我们用边来构建生成树,同时在构建时,需要考虑是否会生成环路.
1、算法思想
Kruskal 算法提供一种在 O(ElogV) 运行时间确定最小生成树的方案。Kruskal 算法基于贪心算法(Greedy Algorithm)的思想进行设计,其选择的贪心策略就是,每次都选择权重最小的但未形成环路的边加入到生成树中。其算法结构如下:
- 将所有的边按照权重非递减排序;
- 选择最小权重的边,判断是否其在当前的生成树中形成了一个环路。如果环路没有形成,则将该边加入树中,否则放弃。
- 重复步骤 2,直到有 V – 1 条边在生成树中。
2、算法分析
Kruskal 算法是以分析边为基础,则需要建立边集数组结构,也就是在程序中需要将邻接矩阵转化为边集数组。
//对边集数组Edge结构的定义 typedef struct { int begin; int end; int weight; }Edge;
程序将邻接矩阵通过程序转化为边集数组,并且对它们的按权值从小到大排序.
3、图例解释
首先第一步,我们有一张图Graph,有若干点和边
将所有的边的长度排序,用排序的结果作为我们选择边的依据。这里再次体现了贪心算法的思想。资源排序,对局部最优的资源进行选择,排序完成后,我们率先选择了边AD。这样我们的图就变成了右图
在剩下的变中寻找。我们找到了CE。这里边的权重也是5
依次类推我们找到了6,7,7,即DF,AB,BE。
下面继续选择, BC或者EF尽管现在长度为8的边是最小的未选择的边。但是现在他们已经连通了(对于BC可以通过CE,EB来连接,类似的EF可以通过EB,BA,AD,DF来接连)。所以不需要选择他们。类似的BD也已经连通了(这里上图的连通线用红色表示了)。
最后就剩下EG和FG了。当然我们选择了EG。最后成功的图就是上图了。
4、代码
//查找连线顶点尾部 int GraphData::FindLastLine(int *parent,int f) { while(parent[f] >0) { f = parent[f]; } return f; } //直接插入排序 void GraphData::InsertSort(Edge *pEdge,int k) { Edge *itemEdge = pEdge; Edge item; int i,j; for (i = 1;i<k;i++) { if (itemEdge[i].weight < itemEdge[i-1].weight) { item = itemEdge[i]; for (j = i -1; itemEdge[j].weight > item.weight ;j--) { itemEdge[j+1] = itemEdge[j]; } itemEdge[j+1] = item; } } } //将邻接矩阵转化为边集数组 void GraphData::GraphToEdges(GraphArray *pArray,Edge *pEdge) { int i; int j; int k; k = 0; for(i = 0; i < pArray->numVertexes; i++) { for(j = i; j < pArray->numEdges; j++) { if(pArray->arg[i][j] < 65535) { pEdge[k].begin = i; pEdge[k].end = j; pEdge[k].weight = pArray->arg[i][j]; k++; } } } printf("k = %d\n", k); printf("边集数组排序前,如下所示.\n"); printf("edges[] beign end weight\n"); for(i = 0; i < k; i++) { printf("%d", i); printf(" %d", pEdge[i].begin); printf(" %d", pEdge[i].end); printf(" %d", pEdge[i].weight); printf("\n"); } //下面进行排序 InsertSort(pEdge, k); printf("边集数组排序后,如下所示.\n"); printf("edges[] beign end weight\n"); for(i = 0; i < k; i++) { printf("%d", i); printf(" %d", pEdge[i].begin); printf(" %d", pEdge[i].end); printf(" %d", pEdge[i].weight); printf("\n"); } } //Kruskal算法(克鲁斯卡尔) void GraphData::MiniSpanTree_Kruskal(GraphArray *pArray) { int i,n,m; int parent[MAXVEX]; //定义边集数组 Edge edges[MAXVEX]; //定义一数组用来判断边与边是否形成环 //邻接矩阵转为边集数组,并按照权值大小排序 GraphToEdges(pArray,edges); for (i =0; i< pArray->numVertexes;i++) { parent[i] = 0; //初始化数组数值为0 } //算法关键实现 for (i = 0;i < pArray->numVertexes;i++) //循环每条边 { //根据边集数组,查找出不为0的边 n = FindLastLine(parent,edges[i].begin); m = FindLastLine(parent,edges[i].end); printf("边%d的开始序号为:%d,结束为:%d)",i,n,m); if(n != m) //假如n与m不等,说明此边没有与现有生成树形成环路 { parent[n] = m; //将此边的结尾顶点放入下标为起点的parent中 //表示此顶点已经在生成树集合中 printf("(%d,%d) %d ", edges[i].begin, edges[i].end, edges[i].weight); } } printf("\n"); }
5、代码分析
在输入14个点位后,根据权值排序得到上图,上图表示边集数组的排序后的结果。
- 在开始(4,5)边的权值最小,在parent中parent[4]与parent[5],都为0,所以返回4,5,两者不相等,此时将parent[4] = 5,此时说明4,5之间有联系了。
- 同样是(2,8),此时parent[2] = 8;
- 继续循环,parent[0] = 1;关键是是边3是应该是(0,5),(之前是parent中0已经有值,继续判断为,此时parent[0] = 1),此时parent[1] = 5.。
- 同样继续循环将图进行输出,填满parent。
- 括号中就是最小生成树的边。
三、总结
克鲁斯卡尔算法的Find函数由边数e决定,时间复杂度为O(loge),而外面有一个for循环e次,所以克鲁斯卡尔算法的时间复杂度为O(eloge)。《此处不包括由邻接矩阵转为边集数组》, 对比两个算法,克鲁斯尔算法主要是针对边来展开,边数少时效率会非常高,所以对于稀疏图有很大的优势;而普里姆算法对于稠密图,即边数非常多的情况会更好一些。