前面的話
本文將詳細介紹圖這種數據結構,包含不少圖的巧妙運用
數據結構
圖是網絡結構的抽象模型。圖是一組由邊連接的節點(或頂點)。圖是重要的,因為任何二元關系都可以用圖來表示
任何社交網絡,例如Facebook、Twitter和Google plus,都可以用圖來表示。還可以使用圖來表示道路、航班以及通信狀態,如下圖所示:
一個圖G = (V, E)由以下元素組成
V:一組頂點
E:一組邊,連接V中的頂點
下圖表示一個圖:
在着手實現算法之前,先了解一下圖的一些術語
由一條邊連接在一起的頂點稱為相鄰頂點。比如,A和B是相鄰的,A和D是相鄰的,A和C是相鄰的,A和E不是相鄰的。
一個頂點的度是其相鄰頂點的數量。比如,A和其他三個頂點相連接,因此,A的度為3;E和其他兩個頂點相連,因此,E的度為2。
路徑是頂點v1,v2,…,vk的一個連續序列,其中vi和vi+1是相鄰的。以上一示意圖中的圖為例,其中包含路徑A B E I和A C D G。
簡單路徑要求不包含重復的頂點。舉個例子,ADG是一條簡單路徑。除去最后一個頂點(因為它和第一個頂點是同一個頂點),環也是一個簡單路徑,比如ADCA(最后一個頂點重新回到A)
如果圖中不存在環,則稱該圖是無環的。如果圖中每兩個頂點間都存在路徑,則該圖是連通的
【有向圖和無向圖】
圖可以是無向的(邊沒有方向)或是有向的(有向圖)。如下圖所示,有向圖的邊有一個方向:
如果圖中每兩個頂點間在雙向上都存在路徑,則該圖是強連通的。例如,C和D是強連通的,而A和B不是強連通的。
圖還可以是未加權的(目前為止我們看到的圖都是未加權的)或是加權的。如下圖所示,加權圖的邊被賦予了權值:
可以使用圖來解決計算機科學世界中的很多問題,比如搜索圖中的一個特定頂點或搜索一條特定邊,尋找圖中的一條路徑(從一個頂點到另一個頂點),尋找兩個頂點之間的最短路徑,以及環檢測
圖的表示
從數據結構的角度來說,有多種方式來表示圖。在所有的表示法中,不存在絕對正確的方式。圖的正確表示法取決於待解決的問題和圖的類型
【鄰接矩陣】
圖最常見的實現是鄰接矩陣。每個節點都和一個整數相關聯,該整數將作為數組的索引。我 們用一個二維數組來表示頂點之間的連接。如果索引為i的節點和索引為j的節點相鄰,則array[i][j] === 1,否則array[i][j] === 0,如下圖所示:
不是強連通的圖(稀疏圖)如果用鄰接矩陣來表示,則矩陣中將會有很多0,這意味着我們浪費了計算機存儲空間來表示根本不存在的邊。例如,找給定頂點的相鄰頂點,即使該頂點只有一個相鄰頂點,我們也不得不迭代一整行。鄰接矩陣表示法不夠好的另一個理由是,圖中頂點的數量可能會改變,而2維數組不太靈活
【鄰接表】
也可以使用一種叫作鄰接表的動態數據結構來表示圖。鄰接表由圖中每個頂點的相鄰頂點列表所組成。存在好幾種方式來表示這種數據結構。我們可以用列表(數組)、鏈表,甚至是散列表或是字典來表示相鄰頂點列表。下面的示意圖展示了鄰接表數據結構
盡管鄰接表可能對大多數問題來說都是更好的選擇,但以上兩種表示法都很有用,且它們有着不同的性質(例如,要找出頂點v和w是否相鄰,使用鄰接矩陣會比較快)
【關聯矩陣】
還可以用關聯矩陣來表示圖。在關聯矩陣中,矩陣的行表示頂點,列表示邊。如下圖所示,使用二維數組來表示兩者之間的連通性,如果頂點v是邊e的入射點,則array[v][e] === 1; 否則,array[v][e] === 0
關聯矩陣通常用於邊的數量比頂點多的情況下,以節省空間和內存
創建Graph類
聲明類的骨架:
function Graph() { var vertices = []; //{1} var adjList = new Dictionary(); //{2} }
使用一個數組來存儲圖中所有頂點的名字(行{1}),以及一個字典來存儲鄰接表(行{2})。字典將會使用頂點的名字作為鍵,鄰接頂點列表作為值。vertices數組和adjList字典兩者都是我們Graph類的私有屬性
接着,將實現兩個方法:一個用來向圖中添加一個新的頂點(因為圖實例化后是空的),另外一個方法用來添加頂點之間的邊
先實現addVertex方法:
this.addVertex = function(v){ vertices.push(v); //{3} adjList.set(v, []); //{4} };
這個方法接受頂點v作為參數。將該頂點添加到頂點列表中(行{3}),並且在鄰接表中,設置頂點v作為鍵對應的字典值為一個空數組(行{4})
現在,來實現addEdge方法:
this.addEdge = function(v, w){ adjList.get(v).push(w); //{5} adjList.get(w).push(v); //{6} };
這個方法接受兩個頂點作為參數。首先,通過將w加入到v的鄰接表中,添加了一條自頂點v到頂點w的邊。如果想實現一個有向圖,則行{5}就足夠了。如果是基於無向圖的,需要添加一條自w向v的邊(行{6})
下面來測試這段代碼:
var graph = new Graph(); var myVertices = ['A','B','C','D','E','F','G','H','I']; //{7} for (var i=0; i<myVertices.length; i++){ //{8} graph.addVertex(myVertices[i]); } graph.addEdge('A', 'B'); //{9} graph.addEdge('A', 'C'); graph.addEdge('A', 'D'); graph.addEdge('C', 'D'); graph.addEdge('C', 'G'); graph.addEdge('D', 'G'); graph.addEdge('D', 'H'); graph.addEdge('B', 'E'); graph.addEdge('B', 'F'); graph.addEdge('E', 'I');
為方便起見,創建了一個數組,包含所有想添加到圖中的頂點(行{7})。接下來,只要遍歷vertices數組並將其中的值逐一添加到我們的圖中(行{8})。最后,添加想要的邊(行{9})。這段代碼將會創建一個圖,也就是到前面的示意圖所使用的
為了更方便一些,下面來實現一下Graph類的toString方法,以便於在控制台輸出圖
this.toString = function(){ var s = ''; for (var i=0; i<vertices.length; i++){ //{10} s += vertices[i] + ' -> '; var neighbors = adjList.get(vertices[i]); //{11} for (var j=0; j<neighbors.length; j++){ //{12} s += neighbors[j] + ' '; } s += '\n'; //{13} } return s; };
我們為鄰接表表示法構建了一個字符串。首先,迭代vertices數組列表(行{10}),將頂點的名字加入字符串中。接着,取得該頂點的鄰接表(行{11}),同樣也迭代該鄰接表(行{12}),將相鄰頂點加入我們的字符串。鄰接表迭代完成后,給我們的字符串添加一個換行符(行{13}),這樣就可以在控制台看到一個漂亮的輸出了。運行如下代碼:
console.log(graph.toString());
輸出如下:
A -> B C D
B -> A E F
C -> A D G D -> A C G H
E -> B I F -> B G -> C D
H -> D I -> E
從該輸出中,頂點A有這幾個相鄰頂點:B、C和D
圖的遍歷
和樹數據結構類似,可以訪問圖的所有節點。有兩種算法可以對圖進行遍歷:廣度優先搜索(Breadth-First Search,BFS)和深度優先搜索(Depth-First Search,DFS)。圖遍歷可以用來尋找特定的頂點或尋找兩個頂點之間的路徑,檢查圖是否連通,檢查圖是否含有環等
在實現算法之前,需要理解圖遍歷的思想方法。圖遍歷算法的思想是必須追蹤每個第一次訪問的節點,並且追蹤有哪些節點還沒有被完全探索。對於兩種圖遍歷算法,都需要明確指出第一個被訪問的頂點
完全探索一個頂點要求我們查看該頂點的每一條邊。對於每一條邊所連接的沒有被訪問過的頂點,將其標注為被發現的,並將其加進待訪問頂點列表中
為了保證算法的效率,務必訪問每個頂點至多兩次。連通圖中每條邊和頂點都會被訪問到
廣度優先搜索算法和深度優先搜索算法基本上是相同的,只有一點不同,那就是待訪問頂點列表的數據結構
算法 數據結構 描 述
深度優先搜索 棧 通過將頂點存入棧中,頂點是沿着路徑被探索的,存在新的相鄰頂點就去訪問
廣度優先搜索 隊列 通過將頂點存入隊列中,最先入隊列的頂點先被探索
當要標注已經訪問過的頂點時,用三種顏色來反映它們的狀態
白色:表示該頂點還沒有被訪問。
灰色:表示該頂點被訪問過,但並未被探索過。
黑色:表示該頂點被訪問過且被完全探索過。
這就是之前提到的務必訪問每個頂點最多兩次的原因
【廣度優先搜索】
廣度優先搜索算法會從指定的第一個頂點開始遍歷圖,先訪問其所有的相鄰點,就像一次訪問圖的一層。換句話說,就是先寬后深地訪問頂點,如下圖所示:
以下是從頂點v開始的廣度優先搜索算法所遵循的步驟
(1) 創建一個隊列Q。
(2) 將v標注為被發現的(灰色),並將v入隊列Q。
(3) 如果Q非空,則運行以下步驟:
(a) 將u從Q中出隊列;
(b) 將標注u為被發現的(灰色);
(c) 將u所有未被訪問過的鄰點(白色)入隊列;
(d) 將u標注為已被探索的(黑色)
下面來實現廣度優先搜索算法:
var initializeColor = function(){ var color = []; for (var i=0; i<vertices.length; i++){ color[vertices[i]] = 'white'; //{1} } return color; }; this.bfs = function(v, callback){ var color = initializeColor(), //{2} queue = new Queue(); //{3} queue.enqueue(v); //{4} while (!queue.isEmpty()){ //{5} var u = queue.dequeue(), //{6} neighbors = adjList.get(u); //{7} color[u] = 'grey'; // {8} for (var i=0; i<neighbors.length; i++){ // {9} var w = neighbors[i]; // {10} if (color[w] === 'white'){ // {11} color[w] = 'grey'; // {12} queue.enqueue(w); // {13} } } color[u] = 'black'; // {14} if (callback) { // {15} callback(u); } } };
廣度優先搜索和深度優先搜索都需要標注被訪問過的頂點。為此,將使用一個輔助數組color。由於當算法開始執行時,所有的頂點顏色都是白色(行{1}),所以可以創建一個輔助函數initializeColor,為這兩個算法執行此初始化操作
下面來深入廣度優先搜索方法的實現。要做的第一件事情是用initializeColor函數來將color數組初始化為white(行{2})。還需要聲明和創建一個Queue實例(行{3}),它將會存儲待訪問和待探索的頂點。bfs方法接受一個頂點作為算法的起始點。起始頂點是必要的,將此頂點入隊列(行{4})。如果隊列非空(行{5}),將通過出隊列(行{6})操作從隊列中移除一個頂點,並取得一個包含其所有鄰點的鄰接表(行{7})。該頂點將被標注為grey(行{8}),表示發現了它(但還未完成對其的探索)。
對於u(行{9})的每個鄰點,取得其值(該頂點的名字——行{10}),如果它還未被訪問過(顏色為white——行{11}),則將其標注為已經發現了它(顏色設置為grey——行{12}),並將這個頂點加入隊列中(行{13}),這樣當其從隊列中出列的時候,可以完成對其的探索。當完成探索該頂點 和其相鄰頂點后,將該頂點標注為已探索過的(顏色設置為black——行{14})
實現的這個bfs方法也接受一個回調。這個參數是可選的,如果傳遞了回調函數(行{15}),會用到它。執行下面這段代碼來測試一下這個算法:
function printNode(value){ //{16}
console.log('Visited vertex: ' + value); //{17}
}
graph.bfs(myVertices[0], printNode); //{18}
首先,聲明了一個回調函數(行{16}),它僅僅在瀏覽器控制台上輸出已經被完全探索過的頂點的名字。接着,調用bfs方法,給它傳遞第一個頂點(A——myVertices數組)和回調函數。執行這段代碼時,該算法會在瀏覽器控制台輸出下示的結果:
Visited vertex: A
Visited vertex: B
Visited vertex: C
Visited vertex: D
Visited vertex: E
Visited vertex: F
Visited vertex: G
Visited vertex: H
Visited vertex: I
頂點被訪問的順序和示意圖中所展示的一致
考慮如何來解決下面這個問題。給定一個圖G和源頂點v,找出對每個頂點u,u和v之間最短路徑的距離(以邊的數量計)。 對於給定頂點v,廣度優先算法會訪問所有與其距離為1的頂點,接着是距離為2的頂點,以此類推。所以,可以用廣度優先算法來解這個問題。可以修改bfs方法以返回給我們一些信息:
從v到u的距離d[u];
前溯點pred[u],用來推導出從v到其他每個頂點u的最短路徑。
下面是改進過的廣度優先方法的實現:
this.BFS = function(v){ var color = initializeColor(), queue = new Queue(), d = [], //{1} pred = []; //{2} queue.enqueue(v); for (var i=0; i<vertices.length; i++){ //{3} d[vertices[i]] = 0; //{4} pred[vertices[i]] = null; //{5} } while (!queue.isEmpty()){ var u = queue.dequeue(), neighbors = adjList.get(u); color[u] = 'grey'; for (i=0; i<neighbors.length; i++){ var w = neighbors[i]; if (color[w] === 'white'){ color[w] = 'grey'; d[w] = d[u] + 1; //{6} pred[w] = u; //{7} queue.enqueue(w); } } color[u] = 'black'; } return { //{8} distances: d, predecessors: pred }; };
還需要聲明數組d(行{1})來表示距離,以及pred數組來表示前溯點。下一步則是對圖中的每一個頂點,用0來初始化數組d(行{4}),用null來初始化數組pred。發現頂點u的鄰點w時,則設置w的前溯點值為u(行{7})。還通過給d[u]加1來設置v和w之間的距離(u是w的前溯點,d[u]的值已經有了)。方法最后返回了一個包含d和pred的對象(行{8})
現在,可以再次執行BFS方法,並將其返回值存在一個變量中:
var shortestPathA = graph.BFS(myVertices[0]); console.log(shortestPathA);
對頂點A執行BFS方法,以下將會是輸出:
distances: [A: 0, B: 1, C: 1, D: 1, E: 2, F: 2, G: 2, H: 2 , I: 3], predecessors: [A: null, B: "A", C: "A", D: "A", E: "B", F: "B", G:"C", H: "D", I: "E"]
這意味着頂點A與頂點B、C和D的距離為1;與頂點E、F、G和H的距離為2;與頂點I的距離為3。通過前溯點數組,可以用下面這段代碼來構建從頂點A到其他頂點的路徑:
var fromVertex = myVertices[0]; //{9} for (var i=1; i<myVertices.length; i++){ //{10} var toVertex = myVertices[i], //{11} path = new Stack(); //{12} for (var v=toVertex; v!== fromVertex; v=shortestPathA.predecessors[v]) { //{13} path.push(v); //{14} } path.push(fromVertex); //{15} var s = path.pop(); //{16} while (!path.isEmpty()){ //{17} s += ' - ' + path.pop(); //{18} } console.log(s); //{19} }
用頂點A作為源頂點(行{9})。對於每個其他頂點(除了頂點A——行{10}),會計算頂點A到它的路徑。從頂點數組得到toVertex(行{11}),然后會創建一個棧來存儲路徑值(行{12})。接着,追溯toVertex到fromVertex的路徑{行{13}}。變量v被賦值為其前溯點的值,這樣能夠反向追溯這條路徑。將變量v添加到棧中(行{14})。最后,源頂點也會被添加到棧中,以得到完整路徑。
這之后,創建了一個s字符串,並將源頂點賦值給它(它是最后一個加入棧中的,所以它是第一個被彈出的項 ——行{16})。當棧是非空的,就從棧中移出一個項並將其拼接到字符串s的后面(行{18})。最后(行{19})在控制台上輸出路徑。執行該代碼段,會得到如下輸出:
A - B A - C A - D A - B - E A - B - F A - C - G A - D - H A - B - E - I
這里,得到了從頂點A到圖中其他頂點的最短路徑(衡量標准是邊的數量)
如果要計算加權圖中的最短路徑(例如,城市A和城市B之間的最 短路徑——GPS和Google Maps中用到的算法),廣度優先搜索未必合適。
舉些例子,Dijkstra’s算法解決了單源最短路徑問題。Bellman–Ford算法解決了邊權值為負的單源最短路徑問題。A*搜索算法解決了求僅一對頂點間的最短路徑問題,它用經驗法則來加速搜索過程。Floyd–Warshall算法解決了求所有頂點對間的最短路徑這一問題。
圖是一個廣泛的主題,對最短路徑問題及其變種問題,有很多的解決方案。但在開始學習這些其他解決方案前,需要掌握好圖的基本概念
【深度優先搜索】
深度優先搜索算法將會從第一個指定的頂點開始遍歷圖,沿着路徑直到這條路徑最后一個頂點被訪問了,接着原路回退並探索下一條路徑。換句話說,它是先深度后廣度地訪問頂點,如下圖所示:
深度優先搜索算法不需要一個源頂點。在深度優先搜索算法中,若圖中頂點v未訪問,則訪問該頂點v。要訪問頂點v,照如下步驟做
1、標注v為被發現的(灰色)。
2、對於v的所有未訪問的鄰點w,訪問頂點w,標注v為已被探索的(黑色)
深度優先搜索的步驟是遞歸的,這意味着深度優先搜索算法使用棧來存儲函數調用(由遞歸調用所創建的棧)
下面來實現一下深度優先算法:
this.dfs = function(callback){ var color = initializeColor(); //{1} for (var i=0; i<vertices.length; i++){ //{2} if (color[vertices[i]] === 'white'){ //{3} dfsVisit(vertices[i], color, callback); //{4} } } }; var dfsVisit = function(u, color, callback){ color[u] = 'grey'; //{5} if (callback) { //{6} callback(u); } var neighbors = adjList.get(u); //{7} for (var i=0; i<neighbors.length; i++){ //{8} var w = neighbors[i]; //{9} if (color[w] === 'white'){ //{10} dfsVisit(w, color, callback); //{11} } } color[u] = 'black'; //{12} };
首先,創建顏色數組(行{1}),並用值white為圖中的每個頂點對其做初始化,廣度優先搜索也這么做的。接着,對於圖實例中每一個未被訪問過的頂點(行{2}和{3}),調用私有的遞歸函數dfsVisit,傳遞的參數為頂點、顏色數組以及回調函數(行{4})
當訪問u頂點時,標注其為被發現的(grey——行{5})。如果有callback函數的話(行{6}),則執行該函數輸出已訪問過的頂點。接下來一步是取得包含頂點u所有鄰點的列表(行{7})。對於頂點u的每一個未被訪問過(顏色為white——行{10}和行{8})的鄰點w(行{9}), 將調用dfsVisit函數,傳遞w和其他參數(行{11}——添加頂點w入棧,這樣接下來就能訪問它)。最后,在該頂點和鄰點按深度訪問之后,我們回退,意思是該頂點已被完全探索,並將其標注為black(行{12})
執行下面的代碼段來測試一下dfs方法:
graph.dfs(printNode);
輸出如下:
Visited vertex: A
Visited vertex: B
Visited vertex: E
Visited vertex: I
Visited vertex: F
Visited vertex: C
Visited vertex: D
Visited vertex: G
Visited vertex: H
這個順序和示意圖所展示的一致。下面這個示意圖展示了該算法每一步的執行過程:
行{4}只會被執行一次,因為所有其他的頂點都有路徑到第一個調用dfsVisit函數的頂點(頂點A)。如果頂點B第一個調用函數,則行{4}將會為其他頂點再執行一次(比如頂點A)
到目前為止,只是展示了深度優先搜索算法的工作原理。可以用該算法做更多的事情,而不只是輸出被訪問頂點的順序
對於給定的圖G,希望深度優先搜索算法遍歷圖G的所有節點,構建“森林”(有根樹的一個集合)以及一組源頂點(根),並輸出兩個數組:發現時間和完成探索時間。可以修改dfs方法來返回一些信息:
頂點u的發現時間d[u];
當頂點u被標注為黑色時,u的完成探索時間f[u];
頂點u的前溯點p[u]。
來看看改進了的DFS方法的實現:
var time = 0; //{1} this.DFS = function(){ var color = initializeColor(), //{2} d = [], f = [], p = []; time = 0; for (var i=0; i<vertices.length; i++){ //{3} f[vertices[i]] = 0; d[vertices[i]] = 0; p[vertices[i]] = null; } for (i=0; i<vertices.length; i++){ if (color[vertices[i]] === 'white'){ DFSVisit(vertices[i], color, d, f, p); } } return { //{4} discovery: d, finished: f, predecessors: p }; }; var DFSVisit = function(u, color, d, f, p){ console.log('discovered ' + u); color[u] = 'grey'; d[u] = ++time; //{5} var neighbors = adjList.get(u); for (var i=0; i<neighbors.length; i++){ var w = neighbors[i]; if (color[w] === 'white'){ p[w] = u; // {6} DFSVisit(w,color, d, f, p); } } color[u] = 'black'; f[u] = ++time; //{7} console.log('explored ' + u); };
需要一個變量來要追蹤發現時間和完成探索時間(行{1})。時間變量不能被作為參數傳遞,因為非對象的變量不能作為引用傳遞給其他JavaScript方法(將變量作為引用傳遞的意思是如果該變量在其他方法內部被修改,新值會在原始變量中反映出來)。接下來,聲明數組d、f和p(行{2})。需要為圖的每一個頂點來初始化這些數組(行{3})。在這個方法結尾處返回這些值(行{4}),之后要用到它們
當一個頂點第一次被發現時,追蹤其發現時間(行{5})。當它是由引自頂點u的邊而被發現的,追蹤它的前溯點(行{6})。最后,當這個頂點被完全探索后,追蹤其完成時間(行{7})
深度優先算法背后的思想是什么?邊是從最近發現的頂點u處被向外探索的。只有連接到未發現的頂點的邊被探索了。當u所有的邊都被探索了,該算法回退到u被發現的地方去探索其他的邊。這個過程持續到發現了所有從原始頂點能夠觸及的頂點。如果還留有任何其他未被發現的頂點,對新源頂點重復這個過程。重復該算法,直到圖中所有的頂點都被探索了
對於改進過的深度優先搜索,有兩點需要注意
1、時間(time)變量值的范圍只可能在圖頂點數量的一倍到兩倍之間
2、對於所有的頂點u,d[u]<f[u](意味着,發現時間的值比完成時間的值小,完成時間意思是所有頂點都已經被探索過了)
在這兩個假設下,有如下的規則:
1≤d[u]<f[u]≤2|V|
如果對同一個圖再跑一遍新的深度優先搜索方法,對圖中每個頂點,會得到如下的發現
給定下圖,假定每個頂點都是一個需要去執行的任務:
這是一個有向圖,意味着任務的執行是有順序的。例如,任務F不能在任務A之前執行。這個圖沒有環,意味着這是一個無環圖。所以,可以說該圖是一個有向無環圖(DAG)
當需要編排一些任務或步驟的執行順序時,這稱為拓撲排序(topologicalsorting,英文亦寫作topsort或是toposort)。在日常生活中,這個問題在不同情形下都會出現。例如,開始學習一門計算機科學課程,在學習某些知識之前得按順序完成一些知識儲備(不可以在上算法I前先上算法II)。在開發一個項目時,需要按順序執行一些步驟,例如,首先得從客戶那里得到需求,接着開發客戶要求的東西,最后交付項目。不能先交付項目再去收集需求
拓撲排序只能應用於DAG。那么,如何使用深度優先搜索來實現拓撲排序呢?在前面的示意圖上執行一下深度優先搜索
graph = new Graph(); myVertices = ['A','B','C','D','E','F']; for(i=0;i<myVertices.length;i++){ graph.addVertex(myVertices[i]); } graph.addEdge('A','C'); graph.addEdge('A','D'); graph.addEdge('B','D'); graph.addEdge('B','E'); graph.addEdge('C','F'); graph.addEdge('F','E'); var result = graph.DFS();
這段代碼將創建圖,添加邊,執行改進版本的深度優先搜索算法,並將結果保存到result變量。下圖展示了深度優先搜索算法執行后,該圖的發現和完成時間
現在要做的僅僅是以倒序來排序完成時間數組,這便得出了該圖的拓撲排序:
B - A - D - C - F - E
注意之前的拓撲排序結果僅是多種可能性之一。如果稍微修改一下算法,就會有不同的結果,比如下面這個結果也是眾多其他可能性中的一個:
A - B - C - D - F - E
這也是一個可以接受的結果
【完整代碼】
Graph類的完整代碼如下所示
function Graph() { var vertices = []; //list var adjList = new Dictionary(); this.addVertex = function(v){ vertices.push(v); adjList.set(v, []); //initialize adjacency list with array as well; }; this.addEdge = function(v, w){ adjList.get(v).push(w); //adjList.get(w).push(v); //commented to run the improved DFS with topological sorting }; this.toString = function(){ var s = ''; for (var i=0; i<vertices.length; i++){ s += vertices[i] + ' -> '; var neighbors = adjList.get(vertices[i]); for (var j=0; j<neighbors.length; j++){ s += neighbors[j] + ' '; } s += '\n'; } return s; }; var initializeColor = function(){ var color = {}; for (var i=0; i<vertices.length; i++){ color[vertices[i]] = 'white'; } return color; }; this.bfs = function(v, callback){ var color = initializeColor(), queue = new Queue(); queue.enqueue(v); while (!queue.isEmpty()){ var u = queue.dequeue(), neighbors = adjList.get(u); color[u] = 'grey'; for (var i=0; i<neighbors.length; i++){ var w = neighbors[i]; if (color[w] === 'white'){ color[w] = 'grey'; queue.enqueue(w); } } color[u] = 'black'; if (callback) { callback(u); } } }; this.dfs = function(callback){ var color = initializeColor(); for (var i=0; i<vertices.length; i++){ if (color[vertices[i]] === 'white'){ dfsVisit(vertices[i], color, callback); } } }; var dfsVisit = function(u, color, callback){ color[u] = 'grey'; if (callback) { callback(u); } console.log('Discovered ' + u); var neighbors = adjList.get(u); for (var i=0; i<neighbors.length; i++){ var w = neighbors[i]; if (color[w] === 'white'){ dfsVisit(w, color, callback); } } color[u] = 'black'; console.log('explored ' + u); }; this.BFS = function(v){ var color = initializeColor(), queue = new Queue(), d = {}, pred = {}; queue.enqueue(v); for (var i=0; i<vertices.length; i++){ d[vertices[i]] = 0; pred[vertices[i]] = null; } while (!queue.isEmpty()){ var u = queue.dequeue(), neighbors = adjList.get(u); color[u] = 'grey'; for (i=0; i<neighbors.length; i++){ var w = neighbors[i]; if (color[w] === 'white'){ color[w] = 'grey'; d[w] = d[u] + 1; pred[w] = u; queue.enqueue(w); } } color[u] = 'black'; } return { distances: d, predecessors: pred }; }; var time = 0; this.DFS = function(){ var color = initializeColor(), d = {}, f = {}, p = {}; time = 0; for (var i=0; i<vertices.length; i++){ f[vertices[i]] = 0; d[vertices[i]] = 0; p[vertices[i]] = null; } for (i=0; i<vertices.length; i++){ if (color[vertices[i]] === 'white'){ DFSVisit(vertices[i], color, d, f, p); } } return { discovery: d, finished: f, predecessors: p }; }; var DFSVisit = function(u, color, d, f, p){ console.log('discovered ' + u); color[u] = 'grey'; d[u] = ++time; var neighbors = adjList.get(u); for (var i=0; i<neighbors.length; i++){ var w = neighbors[i]; if (color[w] === 'white'){ p[w] = u; DFSVisit(w,color, d, f, p); } } color[u] = 'black'; f[u] = ++time; console.log('explored ' + u); }; }
最短路徑算法
設想要從街道地圖上的A點,通過可能的最短路徑到達B點。這種問題在生活中非常常見,會求助於百度地圖等應用程序。當然,也有其他的考慮,如時間或路況,但根本的問題仍然是: 從A到B的最短路徑是什么?
可以用圖來解決這個問題,相應的算法被稱為最短路徑。下面將介紹兩種非常著名的算法,即Dijkstra算法和Floyd-Warshall算法
【Dijkstra算法】
Dijkstra算法是一種計算從單個源到所有其他源的最短路徑的貪心算法,這意味着可以用它來計算從圖的一個頂點到其余各頂點的最短路徑
考慮下圖:
下面來看看如何找到頂點A和其余頂點之間的最短路徑。但首先,需要聲明表示上圖的鄰接矩陣,如下所示:
var graph = [[0, 2, 4, 0, 0, 0], [0, 0, 1, 4, 2, 0], [0, 0, 0, 0, 3, 0], [0, 0, 0, 0, 0, 2], [0, 0, 0, 3, 0, 2], [0, 0, 0, 0, 0, 0]];
現在,通過下面的代碼來看看Dijkstra算法是如何工作的:
this.dijkstra = function(src) { var dist = [], visited = [], length = this.graph.length; for (var i = 0; i < length; i++) { //{1} dist[i] = INF; visited[i] = false; } dist[src] = 0; //{2} for (var i = 0; i < length-1; i++) { //{3} var u = minDistance(dist, visited); //{4} visited[u] = true; //{5} for (var v = 0; v < length; v++) { if (!visited[v] && this.graph[u][v] != 0 && dist[u] != INF && dist[u] + this.graph[u][v] < dist[v]) { //{6} dist[v] = dist[u] + this.graph[u][v]; //{7} } } } return dist; //{8} };
下面是對算法過程的描述
行{1}:首先,把所有的距離(dist)初始化為無限大(JavaScript最大的數INF = Number. MAX_SAFE_INTEGER),將visited[]初始化為false
行{2}:然后,把源頂點到自己的距離設為0
行{3}:接下來,要找出到其余頂點的最短路徑
行{4}:為此,需要從尚未處理的頂點中選出距離最近的頂點
行{5}:把選出的頂點標為visited,以免重復計算
行{6}:如果找到更短的路徑,則更新最短路徑的值(行{7})
行{8}:處理完所有頂點后,返回從源頂點(src)到圖中其他頂點最短路徑的結果
要計算頂點間的minDistance,就要搜索dist數組中的最小值,返回它在數組中的索引:
var minDistance = function(dist, visited) { var min = INF, minIndex = -1; for (var v = 0; v < dist.length; v++) { if (visited[v] == false && dist[v] <= min) { min = dist[v]; minIndex = v; } } return minIndex; };
對前面的圖執行以上算法,會得到如下輸出:
0 0 1 2 2 3 3 6 4 4 5 6
【Floyd-Warshall算法】
Floyd-Warshall算法是一種計算圖中所有最短路徑的動態規划算法。通過該算法,可以找出從所有源到所有頂點的最短路徑
Floyd-Warshall算法實現如下:
this.floydWarshall = function() { var dist = [], length = this.graph.length, i, j, k; for (i = 0; i < length; i++) { //{1} dist[i] = []; for (j = 0; j < length; j++) { dist[i][j] = this.graph[i][j]; } } for (k = 0; k < length; k++) { //{2} for (i = 0; i < length; i++) { for (j = 0; j < length; j++) { if (dist[i][k] + dist[k][j] < dist[i][j]) { //{3} dist[i][j] = dist[i][k] + dist[k][j]; //{4} } } } } return dist; };
下面是對算法過程的描述
行{1}:首先,把dist數組初始化為每個頂點之間的權值,因為i到j可能的最短距離就是這些頂點間的權值
行{2}:通過k,得到i途徑頂點0至k,到達j的最短路徑
行{3}:判斷i經過頂點k到達j的路徑是否比已有的最短路徑更短
行{4}:如果是更短的路徑,則更新最短路徑的值
行{3}是Floyd-Warshall算法的核心。對前面的圖執行以上算法,會得到如下輸出:
0 2 3 6 4 6 INF 0 1 4 2 4 INF INF 0 6 3 5 INF INF INF 0 INF 2 INF INF INF 3 0 2 INF INF INF INF INF 0
其中,INF代表頂點i到j的最短路徑不存在。 對圖中每一個頂點執行Dijkstra算法,也可以得到相同的結果
最小生成樹
最小生成樹(MST)問題是網絡設計中常見的問題。想象一下,公司有幾間辦公室,要以最低的成本實現辦公室電話線路相互連通,以節省資金,最好的辦法是什么?這也可以應用於島橋問題。設想要在n個島嶼之間建造橋梁,想用最低的成本實現所有島嶼相互連通
這兩個問題都可以用MST算法來解決,其中的辦公室或者島嶼可以表示為圖中的一個頂點,邊代表成本。下面有一個圖的例子,其中較粗的邊是一個MST的解決方案
下面將介紹兩種主要的求最小生成樹的算法:Prim算法和Kruskal算法
【Prim算法】
Prim算法是一種求解加權無向連通圖的MST問題的貪心算法。它能找出一個邊的子集,使得其構成的樹包含圖中所有頂點,且邊的權值之和最小
現在,通過下面的代碼來看看Prim算法是如何工作的:
this.prim = function() { var parent = [], key = [], visited = [], length = this.graph.length, i; for (i = 0; i < length; i++){ key[i] = INF; visited[i] = false; } key[0] = 0; parent[0] = -1; for (i = 0; i < length-1; i++) { var u = minKey(key, visited); visited[u] = true; for (var v = 0; v < length; v++){ if (this.graph[u][v] && visited[v] == false && this.graph[u][v] < key[v]){ parent[v] = u; key[v] = this.graph[u][v]; } } } return parent; };
下面是對算法過程的描述
行{1}:首先,把所有頂點(key)初始化為無限大(JavaScript最大的數INF = Number.MAX_ SAFE_INTEGER),visited[]初始化為false
行{2}:其次,選擇第一個key作為第一個頂點,同時,因為第一個頂點總是MST的根節點,所以parent[0] = -1
行{3}:然后,對所有頂點求MST
行{4}:從未處理的頂點集合中選出key值最小的頂點(與Dijkstra算法中使用的函數一樣, 只是名字不同)
行{5}:把選出的頂點標為visited,以免重復計算
行{6}:如果得到更小的權值,則保存MST路徑(parent,行{7})並更新其權值(行 {8})
行{9}:處理完所有頂點后,返回包含MST的結果
比較Prim算法和Dijkstra算法,會發現除了行{7}和行{8}之外,兩者非常相似。行{7}用parent數組保存MST的結果。行{8}用key數組保存權值最小的邊,而在Dijkstra算法中,用dist數組保存距離。可以修改Dijkstra算法,加入parent數組。這樣,就可以在求出距離的同時得到路徑
對如下的圖執行以上算法:
var graph = [[0, 2, 4, 0, 0, 0], [2, 0, 2, 4, 2, 0], [4, 2, 0, 0, 3, 0], [0, 4, 0, 0, 3, 2], [0, 2, 3, 3, 0, 2], [0, 0, 0, 2, 2, 0]];
會得到如下輸出:
Edge Weight 0 - 1 2 1 - 2 2 5 - 3 2 1 - 4 2 4 - 5 2
【Kruskal算法】
和Prim算法類似,Kruskal算法也是一種求加權無向連通圖的MST的貪心算法。現在,通過下面的代碼來看看Kruskal算法是如何工作的:
this.kruskal = function(){ var length = this.graph.length, parent = [], cost, ne = 0, a, b, u, v, i, j, min; cost = initializeCost(); while(ne<length-1) { for(i=0, min = INF;i < length; i++) { for(j=0;j < length; j++) { if(cost[i][j] < min) { min=cost[i][j]; a = u = i; b = v = j; } } } u = find(u, parent); v = find(v, parent); if (union(u, v, parent)){ ne++; } cost[a][b] = cost[b][a] = INF; } return parent; }
下面是對算法過程的描述
行{1}:首先,把鄰接矩陣的值復制到cost數組,以方便修改且可以保留原始值行{7}
行{2}:當MST的邊數小於頂點總數減1時
行{3}:找出權值最小的邊
行{4}和行{5}:檢查MST中是否已存在這條邊,以避免環路
行{6}:如果u和v是不同的邊,則將其加入MST
行{7}:從列表中移除這些邊,以免重復計算
行{8}:返回MST
下面是find函數的定義。它能防止MST出現環路:
var find = function(i, parent){ while(parent[i]){ i = parent[i]; } return i; };
union函數的定義如下:
var union = function(i, j, parent){ if(i != j) { parent[j] = i; return true; } return false; };
這個算法有幾種變體。這取決於對邊的權值排序時所使用的數據結構(如優先隊列),以及圖是如何表示的
