開門見山,本篇博客就介紹圖相關的東西。圖其實就是樹結構的升級版。上篇博客我們聊了樹的一種,在后邊的博客中我們還會介紹其他類型的樹,比如紅黑樹,B樹等等,以及這些樹結構的應用。本篇博客我們就講圖的存儲結構以及圖的搜索,這兩者算是圖結構的基礎。下篇博客會在此基礎上聊一下最小生成樹的Prim算法以及克魯斯卡爾算法,然后在聊聊圖的最短路徑、拓撲排序、關鍵路徑等等。廢話少說開始今天的內容。
一、概述
在博客開頭,我們先聊一下什么是圖。在此我不想在這兒論述圖的定義,當然那些是枯燥無味的。圖在我們生活中無處不在呢,各種地圖,比如鐵路網,公路網等等這都是典型的圖形結構。來點直觀的,我們就以北京的地鐵為例。如果你在北京坐過地鐵,那么對下方的這張圖並不陌生。下方就是一個典型的圖形結構,而且還是連通圖呢。也就是說,你從任意一個地鐵站進去,就可以在其他相連的地鐵站出來。
下方每個地鐵站就是圖的結點,地鐵站與地鐵站之間的連線就是圖的弧,如果我們給弧添加上距離,那么這個距離就是這個弧所對應的權值。比如我們舉個例子,假如大望路站到國貿站的距離是1.5公里。那么我們翻譯成我們圖中的術語就是大望路結點到國貿結點有一條弧,這條弧的權值是1.5公里。當然,從大望路到國貿有多條路徑,那么那條路徑最近呢,這就是我們后面要說的最優路徑了。我們如果想連通每個站點,並且想連接每個站點的權值的和最小,那么就是我們以后要聊的最小生成樹了。
今天我們博客的主題就是如果去存儲下方這種類型的圖,然后對圖中的節點進行遍歷。當然存儲的時候我們要存儲弧度所對應的權值。
當然,上面這個地鐵站的地鐵是比較復雜的,我們就簡單畫一個圖,來模擬一下上述圖的結構即可。然后將該結構進行存儲。然后再基於該存儲結構對圖進行遍歷。圖的物理存儲結構可以分為鄰接矩陣和鄰接鏈表的形式。則圖的搜索分為廣度優先搜索(BSF -- Breadth First Search)和深度優先搜索(DFS -- Depth First Search)。下面這個圖的結構就是我們要存儲以及遍歷的圖。紅色的部分就是每條邊的權值。
二、鄰接矩陣
接下來我們就將上面這個圖存儲下來,當然是使用我們上面提到過的鄰接矩陣或者鄰接鏈表來存儲。在構建圖之前呢,我們依然要先定義圖的協議,因為圖的物理存儲結構分為鄰接矩陣和鄰接鏈表。不同的存儲方式也就對應着構建圖的方式不同,那么圖的BFS與DFS的具體實現也是不同的,但是對外的接口是一致的。還是那句話,面向接口編程。所以我們要先定義完圖的相關接口,然后在給出具體實現。
1.圖的接口的定義
下方代碼片段就是我們圖結構的協議,所有定義的圖結構都要遵循下方的協議。createGraph()方法會根據傳入的參數構建相應存儲結構的圖,breadthFirstSearch()方法對應的就是圖的廣度優先搜索,depthFirstSearch()對應的就是圖的深度優先搜索,displayGraph()就負責將圖的整個存儲結構進行輸出。
還是那句話,因為圖對外的調用接口是一致的,所以我們對於不同的物理存儲結構的圖,我們可以使用同一個測試用例。定義好了下方的協議后,我們就可以根據圖的物理存儲結構,給出具體實現了。
2、圖中關系的輸入
要想構建上面的圖的結構,我們得根據圖所提供的信息來構建相應物理結構的圖。下方就是我們在構建圖結構時,所輸入的信息。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, 也就是我們之前定義的協議。只要是遵循該協議的類的對象都可以作為該函數的參數,所以我們下方這個測試用例是通用的。這也是面向接口編程的好處之一。
下方是上述代碼的測試用例所輸出的結果,如下所示。當然該測試用例也同樣適用於鄰接鏈表實現的圖,前提是要遵循我們之前定義的協議。
三、鄰接鏈表
上面介紹完鄰接矩陣及其相關內容后,我們還要聊一下另一種圖的存儲結構----鄰接鏈表。鄰接鏈表就是數組與鏈表的結合體,也就是將鏈表掛在一維數組中。開門見山,下方就是鄰接鏈表測試用例所輸出的結果。前面的下標其實就是一個一維數組,每個下標后方所跟的鏈就是掛在該下標后方的鏈。鏈中每個節點所存儲的內容是與該數組下標所連接的結點的下標以及權值。下方這個鄰接鏈表存儲的就是上面我們那個圖。
雖然下方的DFS和BFS與上述鄰接矩陣中的DFS和BFS不同,但是規則是按照我們之前聊的規則來的。
1.鄰接鏈表的創建
上面也說了,鄰接鏈表就是將一個個的鏈表掛在一維數組中。在創建鄰接鏈表之前,我們得先創建鄰接鏈表中鏈表所需的結點。下方這個就是我們鄰接鏈表中所需要的結點。data存儲的是所連結點在一維數組中的index,weightNumber存儲的就是權值,preNoteIndex存儲的就是當前結點所在鏈表連接的一維數組的index。next則指向鏈表中的下一個結點。
創建好我們需要的頭結點后,我們就該創建我們的鄰接鏈表了。下方代碼段的createGraph()方法所需的參數與鄰接矩陣對應的方法所需的參數一致。下方函數中第一個循環是初始化一維數組,將每個結點的信息添加到一維數組中,等待着與這些結點相連的結點掛在相應的鏈上。relationDic中記錄着結點與一維數組索引的對應信息。第二個循環是遍歷relation數組,取出每個結點間的關系信息,根據這些信息將相應的結點掛在相應的一維數組每個元素對應的鏈上。
2、鄰接鏈表的廣度優先搜索(BFS)
鄰接鏈表的廣度優先搜索與鄰接矩陣的廣度優先搜索雖然算法一致,但是由於其存儲數據的方式不同,具體實現起來還是有所不同的。因為是BFS, 所以,鄰接鏈表的BFS依然會借助隊列來實現。下方我們采用了隊列加遞歸的方式來實現的BFS。
方法中最外層的if語句塊用來判斷當前方法傳入的索引所對應的結點是否已經被遍歷了,如果未被遍歷則輸出,輸出后將標志位置為true。遍歷完當前結點后,將與該結點相連接的並且未被遍歷的結點進入隊列。然后再遞歸遍歷隊列中未被遍歷的結點。具體代碼如下所示:
3、鄰接鏈表的深度優先搜索(DFS)
下方這段代碼就是鄰接鏈表的深度優先搜索,下方代碼段沒有借用隊列,但是使用了遞歸。因為在遞歸調用函數的過程中,存在遞歸調用棧。棧有着先入后出的特點,上面我們在聊DFS時聊到,深度優先搜索就是一直往下走,走不動了就回退一步繼續尋找可以往下走的路。這個一直往下走其實就是不斷push入棧的過程,而回退一步其實就是pop出棧的步驟。鑒於遞歸過程本身就是一個棧的結構,所以就不需要我們再創建一個棧來實現這個push和pop操作了。下方就是鄰接鏈表的DFS的相關代碼。代碼並不復雜,在此不做過多贅述了。
至此,圖的鄰接矩陣和鄰接鏈表的DFS、BFS就聊完了。當然本篇博客往上貼的代碼只是部分核心代碼,完整的Demo已在github上進行分享。下方就是分享鏈接,下篇博客會聊一下圖的最小生成樹的兩個算法。今天博客就先到這兒。
Github分享地址:https://github.com/lizelu/DataStruct-Swift/tree/master/Graph