【0】README

1)本文旨在给出 ReviewForJob——最小生成树(prim + kruskal)源码实现和分析, 还会对其用到的 技术 做介绍;

2)最小生成树是对无向图而言的:一个无向图G 的最小生成树是一个连通图,且保证该连通图 所含边的权值和最小;

3)要知道 Prim算法(普利姆算法)的基本 idea 就是 迪杰斯特拉算法,下面会介绍,所以我会po 出 迪杰斯特拉算法的 相关介绍;

4)下面的内容(参见迪杰斯特拉算法)转自 http://blog.csdn.net/pacosonswjtu/article/details/52125636 :其实,说白了:广度优先搜索算法(计算无权最短路径) 是基于 拓扑排序算法的,而 迪杰斯特拉算法(计算有权最短路径) 是基于 广度优先搜索算法或者说是它的变体算法;上述三者不同点在于: 拓扑排序算法 和 广度优先搜索算法 使用了 循环队列, 而迪杰斯特拉算法使用了 二叉堆优先队列作为其各自的工具;相同点在于:他们都使用了 邻接表来表示图;所以 普利姆算法 也是基于 广度优先搜索算法的,且要使用二叉堆优先队列用于选取 权值最小的边;

5)需要事先知道的是:寻找最小生成树有两种alg—— 普利姆算法 和 克鲁斯卡尔算法,普利姆算法过程中有且只有一个连通图,而克鲁斯卡尔算法过程中 会形成多个 连通图;且 克鲁斯卡尔算法使用到了路径压缩,提高了find() 操作的效率,文末会讲到;

【1】Prim算法(普利姆算法) prim alg 源码

1)intro:普利姆算法用于在 无向图中寻找 最小生成树,其基本idea 同 迪杰斯特拉算法,上面已经提及过了;

2)与迪杰斯特拉算法不同的地方在于: 权值更新操作,废话不多说,上代码;

补充)普利姆算法的结束标志:当 所有顶点的状态都是已知(known==1)的时候,算法结束;

// 所有点相连的边的权值最小.
// adj:邻接表(图的标准表示方法), table: 计算无权最短路径的配置表,heap:用于选取最小权值的邻接顶点的小根堆.
void prim(AdjList adj, UnWeightedTable table, int startVertex, BinaryHeap heap)
{       int capacity=adj->capacity;Vertex* arrayVertex = adj->array;Vertex temp;Entry* arrayEntry = table->array;int index; // 顶点标识符(从0开始取)int adjVertex;struct HeapNode node;int weight;int i=0; // 记录已知顶点个数( known == 1 的 个数).//step1(初始状态): startVertex 顶点插入堆. startVertex 从1 开始取.node.vertex=startVertex-1; // 插入堆的 node.vertex 从 0 开始取,所以startVertex-1.node.weight=0;insert(heap, node); // 插入堆.arrayEntry[startVertex-1]->dv=0;arrayEntry[startVertex-1]->pv=0;// 初始状态over.// step2: 堆不为空,执行 deleteMin操作. 并将被删除顶点的邻接顶点插入堆.while(!isEmpty(heap)){     if(i == capacity) // 当所有 顶点都 设置为 已知(known)时,退出循环.  {  break;  }  index = deleteMin(heap).vertex;  // index表示邻接表下标,从0开始取,参见插入堆的操作.arrayEntry[index]->known=1; // 从堆取出后,将其 known 设置为1.i++; // 记录已知顶点个数( known == 1 的 个数).temp = arrayVertex[index];       while(temp->next) {adjVertex = temp->next->index; // 顶点index 的邻接节点标识符adjVertex 从1开始取.weight = temp->next->weight; // 顶点index到其邻接顶点 的权值.if(arrayEntry[adjVertex-1]->known == 0) // 注意: 下标是adjVertex-1, 且known==0 表明 adjVertex顶点还处于未知状态,所以adjVertex插入堆.{// prim 算法的代码版本.if(arrayEntry[adjVertex-1]->dv > weight ) // [key code] 当当前权值和 比 之前权值和 小的时候 才更新,否则不更新.{node.vertex=adjVertex-1; // 插入堆的 node.vertex 从 0 开始取.node.weight=weight;insert(heap, node); // 插入堆.arrayEntry[adjVertex-1]->dv = weight; // [also key code]                 arrayEntry[adjVertex-1]->pv=index+1; // index 从0开始取,所以index加1.  }/* dijkstra 算法的代码版本.if(arrayEntry[adjVertex-1]->dv > arrayEntry[index]->dv + weight ) // [key code] 当当前权值和 比 之前权值和 小的时候 才更新,否则不更新.{node.vertex=adjVertex-1; // 插入堆的 node.vertex 从 0 开始取.node.weight=weight;insert(heap, node); // 插入堆.arrayEntry[adjVertex-1]->dv = arrayEntry[index]->dv + weight; // [also key code]                  arrayEntry[adjVertex-1]->pv=index+1; // index 从0开始取,所以index加1.  }*/}            temp = temp->next;}//printWeightedtable(table, 1);  // 取消这行注释可以 follow 迪杰斯特拉 alg 的运行过程.}
} 

对权值更新的分析(Analysis):

A1)普利姆算法的权值更新: 新权值== 其前驱顶点->该顶点的权值(weight);

A2)迪杰斯特拉算法的权值更新:新权值== 其前驱的前驱顶点-> 前驱的权值(arrayEntry[index]->dv) + weight;

【2】Kruskal 算法(克鲁斯卡尔算法)克鲁斯卡尔源码

1)intro:连续地按照最小的权选择边,并且当所选的边不产生圈时就把它作为取定的边;

2)克鲁斯卡尔算法由多个连通图:将多个连通图进行合并,最终只有1个连通图,当添加到 连通图的边足够多时书法终止(如 顶点数目为7, 则当添加的边数为6 则算法终止);事实上,克鲁斯卡尔算法就是要决定边 应该添加还是应该放弃;

3)克鲁斯卡尔算法用到的技术:

tech1)二叉堆优先队列:用于选取权值最小的边(deleteMin(binaryHeap)操作来完成);

tech2)克鲁斯卡尔算法就是要决定边 应该添加还是应该放弃,这里用到了不相交集 ADT 的union/find 操作,当find(v1) 返回的集合标识 和 find(v2) 返回的集合标识不同时,则添加边(v1,v2);否则放弃添加;

// 克鲁斯卡尔算法 用于寻找 最小生成树.
// 为什么这里没有把 邻接表作为参数传进来,因为即使将其作为参数,其还是要转化为 堆,
// 所以,为了算法的简洁性,在调用 kruskal() 方法前 就将 邻接表转化为 二叉堆优先队列了.
void kruskal(BinaryHeap heap, int* setArray, Edge* edgeSet, int edgeNum)  // 当顶点数=7时,edgeNum=6 因为 7个顶点最多6条边.
{int i=0;Edge edge;int set1, set2;while(!isEmpty(heap) && i<edgeNum){edge = deleteMin(heap); // edge 不可能为空,因为heap 不为空(while循环)。set1 = find(setArray, edge->v1);set2 = find(setArray, edge->v2);// 克鲁斯卡尔算法就是要决定边 应该添加还是应该放弃if(set1 != set2) // 如果 v1 和 v2 不属于同一个集合,边就进行合并.{// setUnion begins.edgeSet[i++] = edge; // 添加边.          setArray[set2] = set1; //更新 edge->v2 的根的 集合标识. 而不能写成setArray[edge->v2] = set1;// setUnion over.}}
}   

tech3)为了提高find() 操作的效率,使用到了 路径压缩。为什么需要路径压缩? 因为集合合并涉及两个操作:find 和 merge/setUnion 操作,又 find() 操作的执行效率取决于 树的高度,所以要进行路径压缩,减少树的高度(路径长度)(补充:路径压缩定义——设操作是 find(x),此时路径压缩的效果是 从 x 到 根的路径上的每一个节点(包括x,但不包括根)都作为根的直接儿子) 路径压缩基础参见 路径压缩基础知识

// 寻找 index标识的 顶点 所在的集合.
// find() 涉及到路径压缩,路径压缩 基于 栈来实现(先进后出).
int find(int* setArray, int index)
{int temp = index;int i=0;            while(index != setArray[index]){stack[i++] = index; // index 从 1 开始取,stack 的元素 也从 1 开始取.index = setArray[index]; // setArray 下标从1 开始.}  // 下面进行路径压缩(基于栈的观点). while(--i >= 0){setArray[ stack[i] ] = index;}    return index;
} 

对以上代码的分析(Analysis):路径压缩是基于 不相交集 find 和 setUnion 操作,下面对其做分析

A1)起初,所有顶点都是一个集合,每个集合中只有一个顶点,有多少个顶点就有多少个集合,即setArray[i] = i(要知道0号下标不用的,这样为了编程方便),setArray[i] 中保存的是 仅仅是集合的标志;

A2)对边 执行 setUnion() 即合并之前,要执行find() 操作,查看 边的两个顶点 是否属于同一个集合,好比 v1-weight-v2 == edge 这条边, setArray[v1] == setArray[v2]? 如果它们返回的集合标识符相等,则放弃合并;否则将它们进行合并;

A3)合并也要分两个步骤: step1)将边添加到 edgeSet 集合中;step2)更新 任意一个顶点的根的集合 而不是 一个顶点的集合,这里是很重要的 ;什么叫做根? 但 setArray[i] == i 的时候,我们认为 i标识所在的顶点叫做根;

看个合并荔枝)说了这么多,还不如一个荔枝来的快:再次提醒 当 setArray[i]==i 的时候,i标识的顶点才是根,才是集合的根,或是集合的标识;

看个荔枝)什么叫路径压缩?在源码实现的测试用例中,通过堆的deleteMin 依次选择权值最小的边并添加到 edgeSet中,参考上述调试结果,我们知道选择顺序是 (v1, v4) (v6, v7) (v3, v4)(v1, v2)  (v2, v4) (v1,v3) (v4, v7) (v3, v6) (v5, v7);

下面我们依次来分析 什么叫做路径压缩:

step1)(v1,v4) 被添加后,setArray[4]=1,又 setArray[1]=1(满足根的定义),所以v4 和v1 一样属于集合1;

step2)(v6, v7)被添加后,setArray[7]=6, 又 setArray[6]=6(满足根的定义),所以 v7 和 v6 一样属于集合6;

step3)(v3, v4) 被添加后,setArray[4]=1,setArray[1]=3, setArray[3]=3;因为 顶点4的根是顶点1,所以其根要和顶点3 属于同一个集合,则setArray[1]=3,下次find(4) 的路径为 setArray[4]=1 -> setArray[1]=3 -> setArray[3]=3(满足根的定义,over)

step4)(v1, v2)被添加后,setArray[2]=3,因为顶点3是顶点1 的 根;

step5)(v2,v4) v2 和 v4 属于同一集合,放弃添加;但是我们发现 setArray[4]=3 了,之前setArray[4]=1的,看看之前的结构是 根3->1->4, 再看看之后的结构是 根3->4 , 根3->1,这就是路径压缩的一个荔枝,因为顶点4 离根3 近了,这就提高了 find的查找效率,以前find(4) 的路径为2,现在的路径为1 因为直接就找到 顶点4的根3了;再次提醒,有兴趣的同学可以参考 路径压缩基础知识

ReviewForJob——最小生成树(prim + kruskal)源码实现和分析相关推荐

  1. JAVA源码优化、分析工具

    JAVA源码优化.分析工具 一.11款用于优化.分析源代码的Java工具 1. PMD from http://pmd.sourceforge.net/ PMD能够扫描Java 源代码,查找类似以下的 ...

  2. Dalvik解释器源码到VMP分析

    前言 学习这块的主要目的还是想知道vmp是如何实现的,如何与系统本身的虚拟机配合工作,所以简单的学习了Dalvik的源码并对比分析了数字公司的解释器.笔记结构如下: dalvik解释器分析 dalvi ...

  3. strings.Builder 源码阅读与分析

    strings.Builder源码阅读与分析 背景之字符串拼接 在 Go 语言中,对于字符串的拼接处理有很多种方法,那么那种方法才是效率最高的呢? str := []string{"aa&q ...

  4. 【PX4-AutoPilot教程-1】PX4源码文件目录架构分析

    PX4源码文件目录架构分析 PX4源代码的结构复杂,这是源代码的总目录结构(以v1.13.0为例): Firmware ├─boards ├─build ├─cmake ├─Documentation ...

  5. MediaPlayer源码流程简要分析

    涉及文件目录: \frameworks\base\media\java\android\media\MediaPlayer.java \frameworks\base\media\jni\androi ...

  6. java sofa rpc_sofa-rpc服务端源码的详细分析(附流程图)

    本篇文章给大家带来的内容是关于sofa-rpc服务端源码的详细分析(附流程图),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助. sofa-rpc是阿里开源的一款高性能的rpc框架,这篇 ...

  7. Vue 合同模板_vue源码逐行注释分析+40多m的vue源码程序流程图思维导图

    vue源码业余时间差不多看了一年,以前在网上找帖子,发现很多帖子很零散,都是一部分一部分说,断章的很多,所以自己下定决定一行行看,经过自己坚持与努力,现在基本看完了,差ddf那部分,因为考虑到自己要换 ...

  8. 如何正确使用语音聊天平台源码的用户分析功能

    任何一个平台的运营,都首先要了解自己平台的用户,用户从哪里来.从哪个渠道了解到我们.留存率是多少.充值打赏率是多少.可能有什么阻碍了用户付费--这些都是平台运营的关键,我们无法直接知晓用户心里在想些什 ...

  9. ArrayList源码扩容机制分析

    ArrayList源码&扩容机制分析 发上等愿,结中等缘,享下等福 文章目录 ArrayList源码&扩容机制分析 1. ArrayList 简介 1.1. Arraylist 和 V ...

最新文章

  1. DNS部署(四)之lvs+keepalived+bind架构高可用负载均衡DNS系统
  2. 当微信小程序遇上TensorFlow:Server端实现补充
  3. Beyond Compare 3 许可证密钥被撤销
  4. 阿里云函数计算 FC再次荣获最受观众喜爱奖
  5. 周期信号的傅里叶级数表示
  6. 05精益敏捷项目管理——超越Scrum
  7. 认识和选购极致画质的显示器
  8. jQuery----选择器
  9. MYSQL 自定义排序
  10. 最会说话的人,都有这十种风度
  11. Java基础学习总结(156)——那些年被淘汰的Java技术及框架
  12. sql并行度_SQL Server最大并行度的重要性
  13. 同步异步-阻塞非阻塞
  14. 导师不喜欢自己怎么办?
  15. 未来,你会反感虚拟现实沉浸式广告吗?
  16. LearnOpenGL->立方体贴图
  17. ORACLE FORMS BUILDER的布局和常用ITEMS
  18. DataBinding 大坑总结(网上我暂时搜不到解决方法)
  19. 多线程基础:两种实现方式
  20. TVS管 具体原理和作用

热门文章

  1. P4127 [AHOI2009]同类分布 数位dp + 对状态剪枝
  2. 【LOJ6363】「地底蔷薇」【点双】【指数型生成函数】【扩展拉格朗日反演】【多项式幂函数】
  3. CF1303F - Number of Components(并查集)
  4. CF938G Shortest Path Queries(线性基,线段树分治,并查集)
  5. CERC17 Problem L - Lunar Landscape(差分,坐标系)
  6. Codeforces Round #737 (Div. 2)
  7. 牛客题霸 [括号序列] C++题解/答案
  8. P5664-Emiya家今天的饭【dp】
  9. jzoj1371-假期【RMQ】
  10. 2019-2020 ICPC Asia Hong Kong Regional Contest 补题(部分)