算法與數據結構(五) 普利姆與克魯斯卡爾的最小生成樹(Swift版)


上篇博客我們聊了圖的物理存儲結構鄰接矩陣和鄰接鏈表,然后在此基礎上給出了圖的深度優先搜索和廣度優先搜索。本篇博客就在上一篇博客的基礎上進行延伸,也是關於圖的。今天博客中主要介紹兩種算法,都是關於最小生成樹的,一種是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是已經添加到最小生成樹上的節點。

下方代碼主要分為下方幾步:

  1. 尋找與上次轉正的結點所連的並且不在adjvex數組中的結點,將這些節點添加到候選集合中。
  2. 從候選集合中找出權值最小的那條邊,並且確定與該邊所連的節點可以轉正。
  3. 將上一步尋找的結點添加到我們新的鄰接鏈表中。
  4. 將已經轉正的節點從候選結合中刪除。
  5. 將已經轉正的節點添加進adjves數組中。
  6. 遞歸這個剛剛轉正的節點。

  

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM