算法與數據結構(四) 圖的物理存儲結構與深搜、廣搜(Swift版)

開門見山,本篇博客就介紹圖相關的東西。圖其實就是樹結構的升級版。上篇博客咱們聊了樹的一種,在後邊的博客中咱們還會介紹其餘類型的樹,好比紅黑樹,B樹等等,以及這些樹結構的應用。本篇博客咱們就講圖的存儲結構以及圖的搜索,這二者算是圖結構的基礎下篇博客會在此基礎上聊一下最小生成樹的Prim算法以及克魯斯卡爾算法,而後在聊聊圖的最短路徑、拓撲排序、關鍵路徑等等。廢話少說開始今天的內容。git

 

1、概述github

在博客開頭,咱們先聊一下什麼是圖。在此我不想在這兒論述圖的定義,固然那些是枯燥無味的。圖在咱們生活中無處不在呢,各類地圖,好比鐵路網,公路網等等這都是典型的圖形結構。來點直觀的,咱們就以北京的地鐵爲例。若是你在北京坐過地鐵,那麼對下方的這張圖並不陌生。下方就是一個典型的圖形結構,並且仍是連通圖呢。也就是說,你從任意一個地鐵站進去,就能夠在其餘相連的地鐵站出來。算法

下方每一個地鐵站就是圖的結點,地鐵站與地鐵站之間的連線就是圖的弧,若是咱們給弧添加上距離,那麼這個距離就是這個弧所對應的權值。好比咱們舉個例子,假如大望路站到國貿站的距離是1.5千米。那麼咱們翻譯成咱們圖中的術語就是大望路結點到國貿結點有一條弧,這條弧的權值是1.5千米。固然,從大望路到國貿有多條路徑,那麼那條路徑最近呢,這就是咱們後面要說的最優路徑了。咱們若是想連通每一個站點,而且想鏈接每一個站點的權值的和最小,那麼就是咱們之後要聊的最小生成樹了。編程

今天咱們博客的主題就是若是去存儲下方這種類型的圖,而後對圖中的節點進行遍歷。固然存儲的時候咱們要存儲弧度所對應的權值。數組

  

固然,上面這個地鐵站的地鐵是比較複雜的,咱們就簡單畫一個圖,來模擬一下上述圖的結構便可。而後將該結構進行存儲。而後再基於該存儲結構對圖進行遍歷。圖的物理存儲結構能夠分爲鄰接矩陣和鄰接鏈表的形式。則圖的搜索分爲廣度優先搜索(BSF -- Breadth First Search)深度優先搜索(DFS -- Depth First Search)。下面這個圖的結構就是咱們要存儲以及遍歷的圖。紅色的部分就是每條邊的權值。函數

  

 

2、鄰接矩陣測試

接下來咱們就將上面這個圖存儲下來,固然是使用咱們上面提到過的鄰接矩陣或者鄰接鏈表來存儲。在構建圖以前呢,咱們依然要先定義圖的協議,由於圖的物理存儲結構分爲鄰接矩陣和鄰接鏈表。不一樣的存儲方式也就對應着構建圖的方式不一樣,那麼圖的BFS與DFS的具體實現也是不一樣的,可是對外的接口是一致的。仍是那句話,面向接口編程。因此咱們要先定義完圖的相關接口,而後在給出具體實現。spa

 

1.圖的接口的定義翻譯

下方代碼片斷就是咱們圖結構的協議,全部定義的圖結構都要遵循下方的協議。createGraph()方法會根據傳入的參數構建相應存儲結構的圖,breadthFirstSearch()方法對應的就是圖的廣度優先搜索,depthFirstSearch()對應的就是圖的深度優先搜索,displayGraph()就負責將圖的整個存儲結構進行輸出。3d

仍是那句話,由於圖對外的調用接口是一致的,因此咱們對於不一樣的物理存儲結構的圖,咱們可使用同一個測試用例。定義好了下方的協議後,咱們就能夠根據圖的物理存儲結構,給出具體實現了。

  

 

二、圖中關係的輸入

要想構建上面的圖的結構,咱們得根據圖所提供的信息來構建相應物理結構的圖。下方就是咱們在構建圖結構時,所輸入的信息。allGraphNote數組中存儲的是圖中的全部結點,就相似於某個地鐵站的名字。而relation數組中存儲的就是結點之間的信息。其中一個元組就是一個結點間的關係。(A, B, 10)就說明A到B有條弧,該弧的權值是10,相似於大望路到國貿有條地鐵,距離是1.5同樣。咱們就能夠根據下方的這個信息來構建咱們想構建的圖了。

固然下方信息在鄰接矩陣和鄰接鏈表中的存儲方式是不一樣的,下方會詳細介紹。 而上面咱們提到的createGraph()方法中的兩個參數,就是下方這兩個數組。

  

 

3.鄰接矩陣的構建

鄰接矩陣是存儲圖結構的一種物理存儲方式,其實說白了鄰接矩陣就是一個二維數組,這個二維數組中存儲的是圖中節點的關係。下方這個截圖就是上述圖結構的鄰接矩陣的存儲方式。節點與節點中間若是沒有弧的話,那麼權值就是0。若是兩個節點間有關係的話,那麼其中存儲的就是該弧上的權值,具體以下所示。

  

 

根據上面這個結構,咱們就開始咱們的代碼實現了,下方就是咱們建立鄰接矩陣相應的代碼。createGraph()方法的第一個參數是咱們上面提到過的allGraphNote,也就是圖中全部的結點集合。第二個參數則是上面咱們提到過的relation,其中存儲的就是圖中結點間的關係。下方的initGraph()方法負責存儲圖的鄰接矩陣的初始化,而relationDic中存儲的就是圖的結點與鄰接矩陣下標的對應關係。經過下方這三個函數,咱們就能夠構建出上面圖結構所對應的鄰接矩陣了。

上面這個矩陣其實就是下方這段代碼構建的圖結構的輸出結果。經過輸出結果能夠看出,上面的鄰接矩陣以紅線爲中心軸對稱。由於A到B的的權值爲10,那麼B到A的權值也是10,因此會造成上述對稱結構。這個在咱們對圖的遍歷時須要注意一下該對稱結構。

   

 

4.鄰接矩陣的廣度優先搜索(BFS)

上面建立完鄰接矩陣後,咱們就開始對此鄰接矩陣進行操做了。接下來要乾的事情就是對上面的鄰接矩陣進行廣度優先搜索(Breadth Frist Search)在以前二叉樹的層次遍歷中咱們提到過,二叉樹的層次遍歷與圖的廣度優先搜索就是一個東西。接下來咱們仔細的聊聊。圖的廣度優先搜索要藉助咱們以前聊的隊列。該隊列中記錄的就是上次遍歷那一層節點,下次遍歷結點的順序就按照隊列中記錄的節點的順序來。下方就是廣度搜索的示意圖。

  

上面BFS示意圖中,是以A爲首結點來進行的廣度優先搜索。廣度優先搜索的思想是藉助隊列「一層一層的輸出」。在遍歷一個點後,那麼就將與該結點相連並未遍歷的點加入隊列,下次輸出的點從隊列中獲取,而後再輸出,不斷的重複這個過程。從描述中咱們能夠看出,此過程可使用遞歸來解決。下方代碼段就是鄰接矩陣的廣度優先搜索的代碼,以下所示:

  

上面的代碼並不複雜,上面用到的visited數組用來標記當前遍歷的結點是否已經被遍歷過,由於上述的矩陣是對稱的。代碼比較簡單,在此就不作過多贅述了。主要仍是藉助隊列來保證層級關係。

 

5.鄰接矩陣的深度優先搜索(Depth First Search)

接下來咱們來聊深度優先搜索--DFS。一句話總結DFS,其實就是「一條道走到黑,走不通,退一步再找道」。其實深度優先搜索與以前咱們聊的二叉樹的先序遍歷很是相似。在實現DFS時,若是不使用遞歸來實現的話,咱們能夠藉助棧的操做來實現。由於遞歸原本就是一個棧結構,因此直接可使用遞歸來完成DFS。下方就是DFS的示意圖,下方的示意圖看明白了,用代碼去實現也就不是什麼難事了。

  

下方這個遞歸函數就是鄰接矩陣的DFS的實現,一樣會用到visited來標記結點是否被遍歷過。

  

 

6.測試用例

下方這段代碼就是咱們的測試用例,該測試用例函數的參數的類型是GraphType, 也就是咱們以前定義的協議。只要是遵循該協議的類的對象均可以做爲該函數的參數,因此咱們下方這個測試用例是通用的。這也是面向接口編程的好處之一。

  

下方是上述代碼的測試用例所輸出的結果,以下所示。固然該測試用例也一樣適用於鄰接鏈表實現的圖,前提是要遵循咱們以前定義的協議。

  

 

3、鄰接鏈表

上面介紹完鄰接矩陣及其相關內容後,咱們還要聊一下另外一種圖的存儲結構----鄰接鏈表。鄰接鏈表就是數組與鏈表的結合體,也就是將鏈表掛在一維數組中。開門見山,下方就是鄰接鏈表測試用例所輸出的結果。前面的下標其實就是一個一維數組,每一個下標後方所跟的鏈就是掛在該下標後方的鏈。鏈中每一個節點所存儲的內容是與該數組下標所鏈接的結點的下標以及權值。下方這個鄰接鏈表存儲的就是上面咱們那個圖。

雖然下方的DFS和BFS與上述鄰接矩陣中的DFS和BFS不一樣,可是規則是按照咱們以前聊的規則來的。

  

 

1.鄰接鏈表的建立

上面也說了,鄰接鏈表就是將一個個的鏈表掛在一維數組中。在建立鄰接鏈表以前,咱們得先建立鄰接鏈表中鏈表所需的結點。下方這個就是咱們鄰接鏈表中所須要的結點。data存儲的是所連結點在一維數組中的index,weightNumber存儲的就是權值,preNoteIndex存儲的就是當前結點所在鏈表鏈接的一維數組的index。next則指向鏈表中的下一個結點。

  

 

建立好咱們須要的頭結點後,咱們就該建立咱們的鄰接鏈表了。下方代碼段的createGraph()方法所需的參數與鄰接矩陣對應的方法所需的參數一致。下方函數中第一個循環是初始化一維數組,將每一個結點的信息添加到一維數組中,等待着與這些結點相連的結點掛在相應的鏈上。relationDic中記錄着結點與一維數組索引的對應信息。第二個循環是遍歷relation數組,取出每一個結點間的關係信息,根據這些信息將相應的結點掛在相應的一維數組每一個元素對應的鏈上。

  

 

二、鄰接鏈表的廣度優先搜索(BFS

鄰接鏈表的廣度優先搜索與鄰接矩陣的廣度優先搜索雖然算法一致,可是因爲其存儲數據的方式不一樣,具體實現起來仍是有所不一樣的。由於是BFS, 因此,鄰接鏈表的BFS依然會藉助隊列來實現。下方咱們採用了隊列加遞歸的方式來實現的BFS。

方法中最外層的if語句塊用來判斷當前方法傳入的索引所對應的結點是否已經被遍歷了,若是未被遍歷則輸出,輸出後將標誌位置爲true。遍歷完當前結點後,將與該結點相鏈接的而且未被遍歷的結點進入隊列。而後再遞歸遍歷隊列中未被遍歷的結點。具體代碼以下所示:

  

 

三、鄰接鏈表的深度優先搜索(DFS)

下方這段代碼就是鄰接鏈表的深度優先搜索,下方代碼段沒有借用隊列,可是使用了遞歸。由於在遞歸調用函數的過程當中,存在遞歸調用棧。棧有着先入後出的特色,上面咱們在聊DFS時聊到,深度優先搜索就是一直往下走,走不動了就回退一步繼續尋找能夠往下走的路。這個一直往下走其實就是不斷push入棧的過程,而回退一步其實就是pop出棧的步驟。鑑於遞歸過程自己就是一個棧的結構,因此就不須要咱們再建立一個棧來實現這個push和pop操做了。下方就是鄰接鏈表的DFS的相關代碼。代碼並不複雜,在此不作過多贅述了。

  

 

至此,圖的鄰接矩陣和鄰接鏈表的DFS、BFS就聊完了。固然本篇博客往上貼的代碼只是部分核心代碼,完整的Demo已在github上進行分享。下方就是分享連接,下篇博客會聊一下圖的最小生成樹的兩個算法。今天博客就先到這兒。

Github分享地址:https://github.com/lizelu/DataStruct-Swift/tree/master/Graph

相關文章
相關標籤/搜索