數據結構與算法:圖和圖算法(一)

轉自:https://segmentfault.com/a/1190000010794621

摘 要 : 圖 論 問 題(Graph Theory)


·節點(Vertex)?與?邊(Edge)

·圖的表示:?鄰接表?和?鄰接矩陣

·這里可以分為?有向圖?和無向圖

·無向圖是一種特殊的有向圖

·有權圖?和?無權圖

·圖的遍歷:?DFS?BFS?常見可以解決的問題有:?聯通分量?Flood Fill?尋路?走迷宮?迷宮生成?無權圖的最短路徑環的判斷

·最小生成樹問題(Minimum Spanning Tree)?Prim?Kruskal

·最短路徑問題(Shortest Path)?Dijkstra?Bellman-Ford

·拓撲排序(Topological sorting)

·這里可演示 ->?https://mrpandey.github.io/d3...

什么是圖?

圖是一種復雜的非線性結構。

在線性結構中,數據元素之間滿足唯一的線性關系,每個數據元素(除第一個和最后一個外)只有一個直接前趨和一個直接后繼;

在樹形結構中,數據元素之間有著明顯的層次關系,并且每個數據元素只與上一層中的一個元素(parent node)及下一層的多個元素(孩子節點)相關;

而在圖形結構中,節點之間的關系是任意的,圖中任意兩個數據元素之間都有可能相關。

圖G由兩個集合V(頂點Vertex)和E(邊Edge)組成,定義為G=(V,E)

無向圖 和 有向圖

相關基礎戳這里

有權圖 和 無權圖


頂點的度

對于無向圖,頂點的度表示以該頂點作為一個端點的邊的數目。比如,圖(a)無向圖中頂點V3的度D(V3)=3

對于有向圖,頂點的度分為入度和出度。入度表示以該頂點為終點的入邊數目,出度是以該頂點為起點的出邊數目,該頂點的度等于其入度和出度之和。比如,頂點V1的入度ID(V1)=1,出度OD(V1)=2,所以D(V1)=ID(V1)+OD(V1)=1+2=3

記住,不管是無向圖還是有向圖,頂點數n,邊數e和頂點的度數有如下關系:


因此,就拿有向圖(b)來舉例,由公式可以得到圖G的邊數e=(D(V1)+D(V2)+D(V3))/2=(3+2+3)/2=4


路徑、路徑長度和回路

路徑,比如在無向圖G中,存在一個頂點序列Vp,Vi1,Vi2,Vi3…,Vim,Vq,使得(Vp,Vi1),(Vi1,Vi2),…,(Vim,Vq)均屬于邊集E(G),則稱頂點Vp到Vq存在一條路徑。

一系列頂點構成路徑,路徑中所有頂點都由邊連接。

路徑長度,是指一條路徑上經過的邊的數量。

回路,指一條路徑的起點和終點為同一個頂點。

用圖對現實中的系統建模

可以用圖對現實中許多系統建模。

比如對交通流量建模,頂點可以表示街道的十字路口,邊表示街道。加權的邊可以表示限速或者車道的數量。建模人員可以用這個系統來判斷最佳路線及最有可能堵車的街道。

任何運輸系統都可以用圖來建模。比如,航空公司可以用圖來為其飛行系統建模。將每個機場看成頂點,將經過兩個頂點的每條航線看作一條邊。加權的邊可以看作從一個機場到另一個機場的航班成本,或兩個機場之間的距離,這取決與建模的對象是什么。

包含局域網和廣域網(如互聯網)在內的計算機網絡,同樣經常用圖來建模。

另一個可以用圖來建模的實現系統是消費市場,頂點可以用來表示供應商和消費者。

圖的創建和遍歷

圖的兩種存儲結構(表示圖)

乍看起來,圖和樹或者二叉樹很像,我們可能會嘗試用樹的方式來創建一個圖類,用節點來表示每個頂點。但這種情況下,如果用基于對象的方式去處理就會有問題,因為圖可能增長到非常大。 用對象來表示圖很快會變得效率低下,所以我們要考慮表示頂點或邊的其他方案。

表示頂點

創建圖類的第一步是要創建一個Vertex類保存頂點和邊。這個類的作用與鏈表和二叉搜索樹的Node類一樣。Vertex類有兩個數據成員: 一個用于標識頂點,另一個是表示這個頂點是否被訪問過的布爾值。分別命名為label 和 wasVisited.這個類只需要一個函數,那就是為頂點的數據成員設定值的構造函數。

我們將所有頂點保存到數組中,在圖類里,可以通過它們在數組中位置引用它們。

表示邊

圖的實際信息都保存在邊上,因為它們描述了圖的結構。我們容易像之前提到的那樣用二叉樹的方式去表示圖,這是不對的。二叉樹的表現形式相當固定,一個父節點只能有兩個子節點,而圖結構卻要靈活的多,一個頂點既可以有一條邊,也可以有多條邊與它相連。

我們將表示圖的邊的方法稱為鄰接表?或者鄰接表數組。

這種方法將邊儲存為由頂點的相鄰頂點列表構成的數組,并以此頂點作為索引。

當我們在程序中引用一個頂點時,可以高效地訪問與這個頂點相連的所有頂點的列表。

構建圖

確定了如何在代碼中表圖之后,構建一個表示圖的類就容易了,下面是一個Graph類的定義:


這個類會記錄一個圖表示了多少條邊,并使用一個長度與圖的頂點數相同的數組來記錄頂點數量。

通過for循環為數組中的每個元素添加一個子數組來儲存所有的相鄰頂點,并將所有元素初始化為空字符串。

當調用這個函數并傳入頂點A 和 B 時,函數會先查找頂點A ,函數會先查找A的鄰接表,將頂點B添加到列表中,然后再查找頂點B的鄰接表,將頂點A加入列表。最后,這個函數會將邊數加 1.

showGraph()?函數會通過打印所有頂點及其相鄰頂點列表的方式來顯示圖:


一個完整的?Graph?類


圖的兩種遍歷方法

確定從一個指定的頂點可以到達其他哪些頂點。這是經常對圖執行的操作。我們可能想通過地圖了解到從一個城鎮到另一個城鎮有哪些路,或者從一個機場到其他機場有哪些航班。

而圖上這些操作是用算法執行的。在圖上可以執行以下兩種遍歷算法用于搜索:

深度優先搜索遍歷


深度優先搜索DFS遍歷類似于樹的前序遍歷。其基本思路是:

a) 假設初始狀態是圖中所有頂點都未曾訪問過,則可從圖G中任意一頂點v為初始出發點,首先訪問出發點v,并將其標記為已訪問過。

b)然后依次從v出發搜索v的每個鄰接點w,若w未曾訪問過,則以w作為新的出發點出發,繼續進行深度優先遍歷,直到圖中所有和v有路徑相通的頂點都被訪問到。

c) 若此時圖中仍有頂點未被訪問,則另選一個未曾訪問的頂點作為起點,重復上述步驟,直到圖中所有頂點都被訪問到為止。

簡單的來說,深度優先搜索包括從一條路徑的起始點開始追溯,直到到達最后一個頂點,然后回溯,繼續追溯下一條路徑,直到到達最后的頂點,如此往復,直到沒有路徑為止

這不是在搜索特定的路徑,而是通過搜索來查看在圖中有哪些路徑可以選擇。

圖示如下:


注:紅色數字代表遍歷的先后順序,所以圖(e)無向圖的深度優先遍歷的頂點訪問序列為:V0,V1,V2,V5,V4,V6,V3,V7,V8

如果采用鄰接矩陣存儲,則時間復雜度為O(n2);當采用鄰接表時時間復雜度為O(n+e)。

深度優先搜索的算法比較簡單: 訪問一個沒有訪問過的頂點,將它標記為已訪問,再遞歸地去訪問在起始點的鄰接表中其他沒有訪問過的頂點。

要讓該算法運行,需要為Graph類添加一個數組,用來儲存已訪問過的頂點,將它所有元素的值全部初始化為false。Graph類的代碼片段演示了這個新數組及其初始化過程:


現在我們可以開始編寫深度優先搜索函數:

代碼中用到了print()函數,這樣我們可以查看當前正在訪問的頂點。當然,dfs()不想要print()也能運行。

注意?深度優先算法屬于盲目搜索,無法保證搜索到的路徑為最短路徑。

執行深度優先搜索

完整示例

這里

廣度優先搜索遍歷


廣度優先搜索遍歷BFS類似于樹的按層次遍歷。其基本思路是:

a) 首先訪問出發點Vi

b) 接著依次訪問Vi的所有未被訪問過的鄰接點Vi1,Vi2,Vi3,…,Vit并均標記為已訪問過。

c) 然后再按照Vi1,Vi2,… ,Vit的次序,訪問每一個頂點的所有未曾訪問過的頂點并均標記為已訪問過,依此類推,直到圖中所有和初始出發點Vi有路徑相通的頂點都被訪問過為止。

圖示如下:


因此,圖(f)采用廣義優先搜索遍歷以V0為出發點的頂點序列為:V0,V1,V3,V4,V2,V6,V8,V5,V7

如果采用鄰接矩陣存儲,則時間復雜度為O(n2),若采用鄰接表,則時間復雜度為O(n+e)。

簡單的來說,廣度優先搜索從一個頂點開始,嘗試訪問盡可能靠近它的頂點。本質上這種搜索在圖上是逐層移動的,首先檢查最靠近第一個頂點的層,再逐漸向下移動到離起始頂點最遠的層

廣度優先搜索算法使用了抽象的隊列而不是數組來對已經訪問過的頂點進行排序。算法工作原理如下:

查找與當前頂點相鄰的未訪問頂點,將其添加到已訪問頂點列表及隊列中;

從圖中取下一個頂點v,添加到已訪問的頂點列表

將所有與v相鄰的未訪問頂點添加到隊列。

執行廣度優先搜索


以上程序的輸出結果:

關于廣度優先遍歷的應用

-> d3中的each()api?node.each(function)

查找最短路徑

圖最常見的操作之一就是尋找從一個頂點到另一個頂點的最短路徑.

考慮下面的例子:

假期中,你將在兩個星期的時間里游歷10個旅游城市,去那里最富盛名的景點(1 個),你希望通過最短路徑算法,找出開車游歷10個城市行駛的最小歷程數。

另一個最短路徑問題涉及創建一個計算機網絡時的開銷,其中包括兩臺電腦之間傳遞數據的時間,或者兩臺電腦建立和維護連接的成本。?

最短路徑算法可以幫助確定構建此網絡的最有效方法。

廣度優先搜索對應的最短路徑

在執行廣度優先搜索時,會自動查找從一個頂點到另一個相鄰頂點的最短路徑。

例如:要查找從頂點A到頂點D的最短路徑,我們首先會查找從A到D是否有任何一條單邊路徑,接著查找兩條邊的路徑,以此類推。這正是廣度優先搜索的搜索過程,因此我們可以輕松地修改廣度優先搜索算法,找出最短路徑。

確定路徑

要查找最短路徑,需要修改廣度優先搜索算法來記錄從一個頂點到另一個頂點的路徑,這需要對Gragh類做一些修改。

首先,需要一個數組來保存從一個頂點到下一個頂點的所有邊。我們將這個數組命名為edgeTo。 因為從始至終使用的都是廣度優先搜索函數,所以每次都會遇到一個沒有標記的頂點,除了對它進行標記外,還會從鄰接列表中我們正在搜索的那個頂點添加一條邊到這個頂點。

下面是新的bfs()函數 和需要添加到Gragh類的代碼:

現在我們需要一個函數,用于展示圖中連接到不同頂點的路徑。函數pathTo() 創建了一個棧,用來儲存與指定頂點有共同邊的所有頂點。

以下是pathTo()函數的代碼,以及一個簡單的輔助函數:

需要確保將以下聲明添加到 Graph()構造函數中:

有了這個函數,我們要做的就是編寫一些客戶端代碼來顯示從源頂點到某個特定頂點的最短路徑。

查找一個頂點的最短路徑

以上程序輸出結果為:

0-2-4

也就是從頂點 0 到頂點4 的最短路徑

拓撲排序

在圖論中,拓撲排序(Topological Sorting)是一個有向無環圖(DAG, Directed Acyclic Graph)的所有頂點的線性序列。

該序列必須滿足下面兩個條件:

每個頂點出現且只出現一次

若存在一條從頂點 A 到頂點 B 的路徑,那么在序列中頂點 A 出現在頂點 B 的前面

有向無環圖(DAG)才有拓撲排序,非DAG圖沒有拓撲排序一說。


它是一個 DAG 圖,那么如何寫出它的拓撲排序呢?這里說一種比較常用的方法:

從 DAG 圖中選擇一個 沒有前驅(即入度為0)的頂點并輸出。

從圖中刪除該頂點和所有以它為起點的有向邊。

重復 1 和 2 直到當前的 DAG 圖為空或當前圖中不存在無前驅的頂點為止。后一種情況說明有向圖中必然存在環。


于是,得到拓撲排序后的結果是 { 1, 2, 4, 3, 5 }。

通常,一個有向無環圖可以有一個或多個拓撲排序序列。

拓撲排序的應用

拓撲排序通常用來“排序”具有依賴關系的任務。它與深度優先搜索BFS類似。不同的是,拓撲排序算法不會立即輸出已訪問的頂點,而是訪問當前頂點鄰接表中的所有相鄰頂點,直到這個列表窮盡時,才將當前頂點壓入棧中。

舉一個例子如下圖:


其拓撲排序可以是:

1,2,3,4,5,7,9,10,11,6,12,8

也可以是:

? ? 9,10,11,6,1,12,4,2,3,5,7,8

再比如,如果用一個DAG圖來表示一個工程,其中每個頂點表示工程中的一個任務,用有向邊<A,B>表示在做任務 B 之前必須先完成任務 A。故在這個工程中,任意兩個任務要么具有確定的先后關系,要么是沒有關系,絕對不存在互相矛盾的關系(即環路)。

graph-data-structure

其他經典問題

有向圖周期檢測:

強連通組件圖

以上不少內容來自《數據結構與算法 javascript》這本書

,感覺講的很糟糕 ,也有可能是譯者的問題。 回頭翻一翻其他資料 重新整理下 并且補上相關算法的應用代碼

常見問題

分別用廣度優先遍歷和深度優先遍歷展開下面節點

我們用筆者畫的d3 tree 看看是怎樣的tree結構

https://codepen.io/AlexZ33/pe...點擊預覽


廣度優先遍歷:

深度優先遍歷:

關系型數組轉換成樹形結構對象

期望輸出:

[{"id":1,"value":"1","children":[{"id":4,"value":"4","children":[]},{"id":5,"value":"5","children":[]}]},{"id":3,"value":"3","children":[{"id":2,"value":"2","children":[]}]}]


?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容