上一篇文章我們簡單介紹了一下什么是圖,以及用JS來實現一個可以添加頂點和邊的圖。按照慣例,任何數據結構都不可或缺的一個point就是遍歷。也就是獲取到數據結構中的所有元素。那么圖當然也不例外。這篇文章我們就來看看如何遍歷以及用js來實現圖的遍歷。
首先,有兩種算法可以對圖進行遍歷:廣度優先搜索(BFS)和深度優先搜索(DFS)。圖的遍歷可以用來尋找特定的頂點,可以尋找兩個頂點之間有哪些路徑,檢查圖是否是聯通的,也可以檢查圖是否含有環等等。
在開始代碼之前,我們需要了解一下圖遍歷的思想,也就是說,我們要知道如何去遍歷一個圖,知道了圖遍歷的方法方式,距離實現代碼也就不遠了。
圖遍歷的思想是:
1、必須追蹤每個第一次訪問的節點,並且追蹤有哪些節點還沒有被完全探索。對於BFS和DFS兩種算法,都需要明確給出第一個被訪問的頂點。
2、完全探索一個頂點,要求我們查看該頂點的每一條邊。對於每一條邊所鏈接的沒有被訪問過的頂點,將其標注為被發現的,並將其加入到待訪問頂點列表中。
那么,總結一下上面的兩句話,首先,我們在遍歷一個圖的時候,需要指定第一個被訪問的頂點是什么(也就是我們要在方法中傳入第一個頂點的值)。然后呢.....我們需要知道三個狀態:
一個是還未被訪問的,也就是我還不知道有這么個頂點,也不知道它的邊都去向哪里。
另外一個是已經訪問過但未被探索過,就是說,我知道有這個頂點,但是我不知道它的邊都去向哪里,連接着哪些頂點。
最后一個是訪問過並且完全探索過。也就是我訪問過該頂點,也探索過它有哪些邊,它的邊連接哪些頂點。
那么,我們就會在構造函數中用三種顏色來代表上面的三種狀態,分別是白色(未被訪問),灰色(已經訪問過但未被探索過)和黑色(訪問過並且完全探索過);
還有另外一個要注意的地方,BFS和DFS在算法上其實基本上是一樣的,但是有一個明顯的不同——待訪問頂點的數據結構。BFS用隊列來存儲待訪問頂點的列表,DFS用棧來存儲待訪問頂點的列表。
好了,下面我們來上代碼。(這里不會貼上所有的代碼,只會貼上有關BFS和DFS的相關代碼。)
如果你看到了這里,但是並不覺得自己可以耐心的把下面的代碼看完,那么你看到這里就可以 結束所有有關於用js來實現數據結構的內容了。如果你還是想繼續往下學習,那么希望你一定可以耐心看完整。
//引入前面章節學過的棧和隊列,因為我們后面會用到。 function Stack () {}; function Queue() {}; function Graph() { var vertices = []; var adjList = new Map(); //添加頂點的方法。 this.addVertices = function (v) {}; this.addEdge = function (v,w) {}; this.toString = function () {}; //初始化圖中各頂點的狀態(顏色)的私有方法,並返回該狀態數組。 var initializeColor = function () { var color = []; for (var i = 0; i < vertices.length; i++) { color[vertices[i]] = 'white'; } return color; }; //簡單的廣度優先搜索算法,傳入參數v是圖中的某一個頂點,從此頂點開始探索整個圖。 this.bfs = function (v,callback) { //為color狀態數組賦值,初始化一個隊列 var color = initializeColor(),queue = new Queue(); //將我們傳入的頂點v入隊。 queue.enqueue(v); // 如果隊列非空,也就是說隊列中始終有已發現但是未探索的頂點,那么執行邏輯。 while(!queue.isEmpty()) { // 隊列遵循先進先出的原則,所以我們聲明一個變量來暫時保存隊列中的第一個頂點元素。 var u = queue.dequeue(); // adjList是我們的鄰接表,從鄰接表中拿到所有u的鄰接頂點。 neighbors = adjList.get(u); //並把狀態數組中的u的狀態設置未已發現但是未完全探索的灰色狀態。 color[u] = 'grey'; //我們循環當前的u的所有的鄰接頂點,並循環訪問每一個鄰接頂點並改變它的狀態為灰色。 for(var i = 0; i < neighbors.length; i++) { var w = neighbors[i]; if (color[w] === "white") { color[w] = 'grey'; //入隊每一個w,這樣while循環會在隊列中沒有任何元素,也就是完全訪問所有頂點的時候結束。 queue.enqueue(w); } } // 完全訪問后設置color狀態。 color[u] = 'black'; // 如果存在回調函數,那么就執行回掉函數。 if(callback) { callback(u); } } }; //改進后計算最短路徑的BFS // 其實這里改進后的BFS並沒有什么特別復雜,只是在原有的bfs的基礎上,增加了一些需要計算和儲存的狀態值。 // 也就是我們在函數結束后所返回的 this.BFS = function (v) { //d是你傳入的頂點v距離每一個頂點的距離(這里的距離僅為邊的數量) //pred就是當前頂點沿着路徑找到的前一個頂點是什么。沒有就是null var color = initializeColor(),queue = new Queue(),d = [],pred = []; //我們把v入隊。 queue.enqueue(v); //初始化距離和前置點數組。一個都為0,一個都為null,無需解釋。 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(var i = 0; i < neighbors.length; i++) { var w = neighbors[i]; if (color[w] === "white") { color[w] = 'grey'; // 到這里都和bfs方法是一樣的,只是多了下面這兩個。 // 這里容易讓人迷惑的是w和u分別是啥?弄清楚了其實也就沒啥了。 // u是隊列中出列的一個頂點,也就是通過u來對照鄰接表找到所有的w。 // 那么因為是d(距離,初始為0)。所以我們只要在d的數組中w的值設為比u大1也就是d[u] + 1就可以了 d[w] = d[u] + 1; // 而這個就不用說了,理解了上面的,這個自然就很好懂了。 pred[w] = u; // 這里可能大家會問,循環不會重復加入么?不會! // 注意看這里if (color[w] === "white")這句,如果是white狀態才會執行后面的邏輯, // 而進入邏輯后,狀態就隨之改變了,不會再次訪問到訪問過的頂點。 queue.enqueue(w); } } color[u] = 'black'; } return { distances:d, predecessors:pred } }; //深度優先搜索 // 這個沒啥東西大家自己看一下就可以了 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); } } }; //深度優先搜索私有方法 // 從dfs中傳入的三個參數 var dfsVisit = function (u,color,callback) { // 改變u的顏色狀態 color[u] = 'grey'; if(callback) {callback(u);} // 獲取所有u的鄰接頂點 var neighbors = adjList.get(u); // 循環 for(var i = 0; i < neighbors.length; i++) { //w為u的每一個鄰接頂點的變量 var w = neighbors[i]; // 如果是白色的我們就遞歸調用dfsVisit if(color[w] === 'white') { dfsVisit(w,color,callback); } } color[u] = 'black'; }; //改進后的DFS,其實也就是加入了更多的概念和要記錄的值 this.DFS = function () { // d,發現一個頂點所用的時間。f,完全探索一個頂點所用的時間,p前溯點。 var color = initializeColor(),d = [],f = [], p = []; // 初始化時間為0; time = 0; //初始化所有需要記錄的對象的值/ for(var i = 0; i < vertices.length; i++) { f[vertices[i]] = 0; d[vertices[i]] = 0; p[vertices[i]] = null; } for (var 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 } }; //注意這里我們為什么要在外層定義時間變量,而不是作為參數傳遞進DFSVisit。 //因為作為參數傳遞在每次遞歸的時候time無法保持一個穩定變化的記錄。 var time = 0; //這里個人覺得也沒什么好說的了,如果你看不懂,希望你可以數據結構系列的第一篇看起。 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); }; }
上面是有關於BFS和DFS的代碼及注釋。希望大家可以認真耐心的看完。下面我們來看看簡單的最短路徑算法和拓撲排序。
1、最短路徑算法
//最短路徑,也就是說我們在地圖上,想要找到兩個點之間的最短距離(我們經常會用地圖軟件來搜索此地與彼地的路徑)。 //那么下面我們就以連接兩個頂點之間的邊的數量的多少,來計算一下各自的路徑,從而得到一個最短路徑。 // 我們通過改進后的BFS算法,可以得到下面這樣的數據,各個頂點距離初始頂點的距離以及前溯點 var shortestPathA = graph.BFS(verticesArray[0]); console.log(shortestPathA) /* 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"] */ //我們選擇數組中的第一個元素為開始的頂點。 var fromVertex = verticesArray[0]; for(var i = 1; i < verticesArray.length;i++) { // 到達的定點不定 var toVertex = verticesArray[i]; //聲明路徑為一個初始化的棧。 path = new Stack(); //嘿嘿,這個循環比較有趣了,通常大家都會用var i= 0; i < xxx;i++這種。 //但是這里這么用是幾個意思?首先大家要知道for循環中兩個“;”所分割的三個語句都是什么意思。 //語句 1 在循環(代碼塊)開始前執行,語句 2 定義運行循環(代碼塊)的條件,語句 3 在循環(代碼塊)已被執行之后執行 //所以我們怎么寫都是可以的!!當然你要符合你想要的邏輯 //后面就不說了,沒啥好說的。 for(var v = toVertex;v!== fromVertex;v = shortestPathA.predecessors[v]) { path.push(v); } path.push(fromVertex); var s = path.pop(); while(!path.isEmpty()) { s += '-' + path.pop(); } console.log(s) } /* A-B A-C A-D A-B-E A-B-F A-C-G A-D-H A-B-E-I */
2、拓撲排序
拓撲排序,想了想,還是有必要給大家解釋一下概念再開始代碼,不然真的容易一臉懵逼。
大家先來看張圖:
那,這是一個什么東西呢?這是一個有向圖,因為邊是有方向的,這個圖沒有環,意味着這是一個無環圖。所以這個圖可以稱之為有向無環圖。那么有向無環圖可以做什么呢?我記得前面某一篇文章說過,所有的實例都有其所面對的要解決的實際問題。而有向無環圖可以視作某一個序列的待執行的任務,該任務不是可跳躍的。比如一個產品上線,需要產品經理定需求,畫流程圖,再到UI出效果圖標注圖再到開發再到測試再到改bug再到上線。就是這個意思。
那么我們上面所形容的產品上線的整個流程就成為拓撲排序。拓撲排序只能應用於DAG(有向無環圖)。
那么我們看下代碼。
//重新聲明一個圖並所有的頂點加入圖中。 var DFSGraph = new Graph(); var DFSarray = ["a","b","c","d","e","f"]; for (var i = 0; i < DFSarray.length; i++) { DFSGraph.addVertices(DFSarray[i]); } //我們為圖加上邊。 DFSGraph.addEdge("a","c"); DFSGraph.addEdge("a","d"); DFSGraph.addEdge("b","d"); DFSGraph.addEdge("b","e"); DFSGraph.addEdge("c","f"); DFSGraph.addEdge("f","e"); var result = DFSGraph.DFS(); console.log(result); //大家自己去看看打印的結果是什么。
那么到這里,有關於圖的一部分內容基本上就都講解完畢了。可能大家覺得我有些偷懶,注釋寫的沒有以前那么詳細了啊。這是因為我覺得很多的內容前面都已經很詳細的說明過了。同樣的思路實在是沒必要翻來覆去的說來說去。所以反而到后面一些復雜的數據結構並沒有前面解釋的那么詳細。但是我覺得如果你一路看下來,這點東西絕壁難不倒你。
最后,由於本人水平有限,能力與大神仍相差甚遠,若有錯誤或不明之處,還望大家不吝賜教指正。非常感謝!