上篇博客我们聊了图的物理存储结构邻接矩阵和邻接链表,然后在此基础上给出了图的深度优先搜索和广度优先搜索。本篇博客就在上一篇博客的基础上进行延伸,也是关于图的。今天博客中主要介绍两种算法,都是关于最小生成树的,一种是Prim算法,另一个是Kruskal算法。这两种算法是很经典的,也是图中比较重要的算法了。
今天博客会先聊一聊Prim算法是如何生成最小生成树的,然后给出具体步骤的示例图,最后给出具体的代码实现,并进行测试。当然Kruskal算法也是会给出具体的示例图,然后给出具体的代码和测试用例。当然本篇博客中的Demo是在上篇博客的基础上进行实现的。因为在上篇博客中我们已经创建好了现成的图了,本篇博客就拿过来直接使用。
在本篇博客的开头呢,先简单的聊一下什么是最小生成树。最小生成树是原图的最小连通子图,也就是说该子图是连通的并且没有多余的边,更不会形成回路。最重要的是最小生成树的所有边的权值相加最小,这也是最小生成树的来源。与现实生活中联系起来那就是一些村庄要通电话线,如何让每个村都可以通电话线并且最省材料。换句话说,是每个村庄连通,并且总线路最短,如果线连接完毕后,其实就是我们本篇博客要聊的最小生成树。
一、普利姆算法
接下来我们就来聊Prim算法。其实Prim算法创建最小生成树的主要思路就是从候选节点中选择最小的权值添加到最小生成树中。下图是我们之前创建的图使用Prim算法创建最小生成树的完整过程。红色的边就是每一步所对应的候选节点做连的弧,从这些候选的边中选出权值最小的边添加到最小生成树中,我们可以将其视为转正的过程。
一个节点转正后,将其转正节点所连的弧度视为候选弧度,当然这些候选弧度所连的节点必须是最小生成树上以外的点。如果候选弧度所连的点位于最小生成树上,那么将该候选节点抛弃。直到无候选弧度时,最小生成树的创建就完成了。
下图就很好的表述了这个过程,每一步候选节点间的连接使用红色标记,而转正的节点间的弧度使用黑色表示。按照下方这个思路,最终就会生成我们需要的最小生成树。
1.Prim算法示意图解析
-
(0):就是我们上篇博客所创建的图的结构,并且每条弧度都有权值。
-
(1):我们以A节点为最小生成树的根节点来创建最小生成树,与A节点相连的是B和F节点,所以这两个节点是本步骤的候选节点。因为(A--10--B) < (A--11--F),所以我们将候选节点中权值最小的B结点进行转正。
-
(2):将B转正,并且使用黑线进行标注,现在A, B节点都位于最小生成树中。B节点转正后,我们将那些与B节点相连但不在最小生成树中的节点添加到候选节点的集合中,此时最小生成树的候选节点有: (B--18--C),(B--12--I), (B--16--G),(A--11--F)。
-
(3):从上一步留下的候选节点中,我们可以看出 A--11--F 这条边的权值最小,所以将F结点转正加入到最小生成树中。因为E结点又与刚转正的F结点相连接,所以将E节点添加进候选结点集合中。此时最小生成树的候选节点有: (B--18--C),(B--12--I), (B--16--G),(F--17--G), (F--26--E)。
-
(4):其中B--12--I这条与候选结点所连的边的权值最小,我们将I转正,并且将于I连的D节点添加进候选节点中。此时最小生成树的候选节点有: (B--18--C), (B--16--G),(F--17--G), (F--26--E),(I--8--C), (I--21--D)。
-
(5):此刻的候选结点有C, G, E, D。因为I -- 8 -- C在候选结点中的弧度最小,所以讲C进行转正。因为C节点转正,所以将到C节点的候选结点移除。将与C节点连接的点添加进行候选结点集合中,此时最小生成树的候选弧度有: (B--16--G),(F--17--G), (F--26--E), (I--21--D),(C--22--D)。
-
(6):从上述候选弧度中,我们容易看出(B--16--G)的权值最小,所以讲G节点进行转正。G节点转正后,那么候选节点的集合为:(F--26--E), (I--21--D),(C--22--D),(G--19--H),(G--24--D)。
-
(7):还是选最小的将其转正,上述候选集合中最小的权值就是(G--19--H),所以讲结点H转正。将(H--7--E), (H--16--D)添加到候选集合当中,此时候选集合为:(F--26--E), (I--21--D),(C--22--D),(G--24--D),(H--7--E), (H--16--D)。
-
(8):上述候选集中(H--7--E)最小,所以将E结点进行转正,E节点转正后的候选节点为:(I--21--D),(C--22--D),(G--24--D),(H--16--D),(E--20--D)。
-
(9):在候选集合中通往D节点的权值最小的是(H--16--D),所以D节点转正,与H节点相连。因为D节点已转正,那么候选节点中所有到达D节点的弧度都得从候选节点中进行移除,那么此刻候选集合为空。当候选集合为空时,就说明我们的最小生成树就生成完毕了。
-
(10):就是我们最终生成的最小生成树。
2.上述过程的代码实现
如果理解了上述过程,那么给出代码的实现并不困难。我们以邻接链表为例,邻接矩阵的最小生成树的Prim算法的表示方式在此就不做过多赘述了,不过Github上分享的Demo是有关于邻接矩阵的Prim算法的相关内容的。下方这个代码截图就是Prim算法在邻接链表中的具体实现。
在下方截图的方法中,第一个参数index是上次转正添加到最小生成树的节点在邻接链表的数组中的索引。第二个参数leafNotes是可以转正的候选叶子结点。第三个参数adjvex是已经添加到最小生成树上的节点。
下方代码主要分为下方几步:
-
寻找与上次转正的结点所连的并且不在adjvex数组中的结点,将这些节点添加到候选集合中。
-
从候选集合中找出权值最小的那条边,并且确定与该边所连的节点可以转正。
-
将上一步寻找的结点添加到我们新的邻接链表中。
-
将已经转正的节点从候选结合中删除。
-
将已经转正的节点添加进adjves数组中。
-
递归这个刚刚转正的节点。
3.测试结果
下方就是我们上述代码所创建的最小生成树,当然我们依然是采用邻接链表来存储我们的最小生成树,下方这个结构就是我们的最小生成树的邻接链表的存储结构,以及对该最小生成树的遍历的结果。
上述是邻接链表上生成的最小生成树以及遍历的结果,下方是邻接矩阵生成的最小生成树以及遍历的结果。
二、克鲁斯卡尔算法
上一部分我们详细的讲解了Prim算法的整个过程,接下来就来聊一下最小生成树的另一个经典的算法Kruskal算法。 Kruskal算法的核心思想就是先将每条边按着权值从小到大进行排序,然后从有序的集合中以此取出最小的边添加到最小生成树中,不过要保证新添加的边与最小生成树上的边不构成回路。下方会给出具体的算法步骤并且给出具体的代码实现。
1.Kruskal算法原理图
首先我们得给节点间的关系也就是我们之前用到的relation数组进行排序,按照权值的大小依次排序,下方就是我们排序的结果。我们构建“最小生成树”所需要的边就从下方的关系中依次取出,在加入最小生成树之前,我们要先判断取出的边加入最小生成树中后是否构成回路。如果不构成回路就添加进最小生成树中,如果构成回路,那么就将该边抛弃。下方就是我们按照权值排好的关系集合。
下方就是从上述集合中取出边,一个一个的往新的邻接链表中插入数据,插入边时我们要判断是否会在最小生成树中形成回路,如果形成回路,那么就将该边抛弃并获取下一条边。
2.寻找节点的尾部节点
在上述算法中,判断新添加的边是否在最小生成树中构成回路是该算法的关键。下方就是判断要连接的两个节点是否在最小生成树中形成回路,当两个节点的尾部节点不相等时,就说明将两个点相连接后不会在最小生成树中构成回路。当两个节点有着共同的尾部节点时,就说明连接后会在最小生成树中形成回路,原理图如下所示:
下方这个方法就是寻找一个节点的尾部节点,parent中存储的就是索引对应节点的尾部节点的索引,下方代码片段就是将寻找的该节点的尾部节点的索引进行返回。
3、Kruskal算法的具体实现
下方代码段就是Kruskal算法的具体实现,首先我们先通过configMiniTree()方法来初始化一个邻接链表,此邻接链表用来存储我们的最小生成树。然后我们对节点与弧度的集合根据权值从小到大排序。排序后,通过for循环对这个有序的集合进行遍历,将那些不构成回路的边添加进我们的最小生成树即可。具体代码如下所示。
1 /** 2 创建最小生成树: Kruskal 3 */ 4 func createMiniSpanTreeKruskal(){ 5 print("克鲁斯卡尔算法:") 6 configMiniTree() 7 //对权值从小到大进行排序 8 let sortRelation = relation.sorted { (item1, item2) -> Bool in 9 return Int(item1.2 as! NSNumber) < Int(item2.2 as! NSNumber) 10 } 11 12 //记录节点的尾部节点,避免出现闭环 13 var parent = Array.init(repeating: -1, count: miniTree.count) 14 15 for item in sortRelation { 16 let beginNoteIndex = self.relationDic[item.0 as! String]! 17 let endNoteIndex = self.relationDic[item.1 as! String]! 18 let weightNumber = item.2 as! Int 19 20 let preEndIndex = findEndIndex(parent: parent, index: beginNoteIndex) 21 let nextEndIndex = findEndIndex(parent: parent, index: endNoteIndex) 22 23 print("\(beginNoteIndex)--\(weightNumber)-->\(endNoteIndex)") 24 25 if preEndIndex != nextEndIndex { 26 27 parent[preEndIndex] = nextEndIndex //更新尾部节点 28 insertNoteToMiniTree(preIndex: beginNoteIndex, 29 linkIndex: endNoteIndex, 30 weightNumber: weightNumber); 31 } 32 } 33 34 displayGraph(graph: miniTree) 35 } 36 37 ///将合适的节点插入到新的邻接链表中 38 private func insertNoteToMiniTree(preIndex: Int, 39 linkIndex: Int, 40 weightNumber: Int) { 41 let note = GraphAdjacencyListNote(data: linkIndex as AnyObject, 42 weightNumber: weightNumber, 43 preNoteIndex: preIndex) 44 note.next = miniTree[preIndex].next 45 miniTree[preIndex].next = note 46 }
篇幅有限,今天博客就先到这吧,本篇博客的完整Demo依然会在github上进行分享,分享地址如下:
github分享地址:https://github.com/lizelu/DataStruct-Swift/tree/master/Graph
请发表评论