學習JavaScript數據結構與算法 — 深度優先搜索算法

深度優先搜索(DFS)

上一次已經提到,的遍歷通常有兩種算法,即廣度優先和深度優先。其中深度優先搜索算法會從第一個指定的頂點開始遍歷圖,沿着路徑直到這條路徑最後一個頂點,接着原路回退並探索下一條路徑。換句話說,它是先深度後廣度地訪問頂點,以下圖1。javascript

圖1java

clipboard.png

與廣度優先算法不一樣,深度優先算法不須要一個起始頂點,只要頂點v沒有被訪問,就訪問該頂點。
咱們用三種狀態來反映頂點的狀態:算法

  • 白色:表示該頂點尚未被訪問。
  • 灰色:表示該頂點被訪問過,但並未被探索過。
  • 黑色:表示該頂點被訪問過且被徹底探索過。

訪問某一頂點v的步驟以下:segmentfault

  • 標註v爲被發現的(灰色)
  • 對於v的全部未訪問的鄰點w:訪問頂點w
  • 標註v爲已被探索的(黑色)

所以深度優先搜索的步驟是遞歸的,這意味着深度優先搜索算法使用棧來存儲函數調用(由遞歸調用所建立的棧)。
與廣度優先算法相似,在算法開始以前,咱們須要將全部頂點置爲白色(本文的全部代碼都是在中實現的Graph類中添加的)。函數

function Graph() {
        var vertices = [];
        var adjList = new Dictionary();
        // 圖類的其餘代碼省略...
        var initializeColor = function(){
            var color = [];
            for (var i=0; i<vertices.length; i++){
                color[vertices[i]] = 'white';
            }
            return color;
        };
    }

接下來實現深度優先算法的核心代碼:this

this.dfs = function(callback){
        var color = initializeColor(); // 將全部頂點初始化爲白色
        for (var i=0; i<vertices.length; i++){
            if (color[vertices[i]] === 'white'){ // 對每個沒有被訪問過的頂點調用dfsVisit方法
                dfsVisit(vertices[i], color, callback); // {1}
            }
        }
        function dfsVisit(u, color, callback){
            color[u] = 'grey'; // 將頂點u置爲灰,代表訪問過但尚未徹底探索
            callback && callback(u); // 執行回調函數
            var neighbors = adjList.get(u); // 獲取頂點u的全部相鄰頂點
            for (var i=0; i<neighbors.length; i++){ // 探索全部相鄰頂點
                var w = neighbors[i];
                if (color[w] === 'white'){ // 若是相鄰的頂點沒有被訪問過,則對其執行dfsVisit方法
                    dfsVisit(w, color, callback);
                }
            }
            color[u] = 'black'; // 將頂點u置爲黑,代表已經徹底訪問。
        };
    };

對圖1中的圖執行上述搜索算法的過程如圖2。spa

圖23d

clipboard.png

因爲咱們是從起始點A開始搜索的,{1}處的代碼只會執行一次,由於全部其餘頂點都有一條路徑到達A點。若是從B點開始搜索,{1}處的代碼便會執行兩次。code

搜索時間

給定一個圖G,要獲得任意頂點u的發現時間(d[u])以及完成對該頂點的搜索時間(f[u]),能夠在上述代碼的基礎上作必定修改(修改的位置用空行隔開):blog

this.DFS = function(){
        var color = initializeColor(), //{2}
            len = vertices.length,

            d = new Array(len).fill([]), // 用於保存任意頂點u的發現時間
            f = new Array(len).fill([]), // 用於保存任意頂點u探索完成的時
            time = 0; // 用於記錄探索時間

        for (var i=0; i<vertices.length; i++){
            if (color[vertices[i]] === 'white'){
                DFSVisit(vertices[i], color, d, f);
            }
        }

        return { // 返回探索數據
            discovery: d,
            finished: f
        };
        function DFSVisit(u, color, d, f){
            console.log('discovered ' + u);
            color[u] = 'grey';

            d[u] = ++time; // 發現頂點u後,時間+1

            var neighbors = adjList.get(u);
            for (var i=0; i<neighbors.length; i++){
                var w = neighbors[i];
                if (color[w] === 'white'){
                    DFSVisit(w,color, d, f);
                }
            }
            color[u] = 'black';
            
            f[u] = ++time; // 探索完頂點u後,時間+1
            
            console.log('explored ' + u);
        };
    };

對於深度優先算法,邊是從最近發現的頂點u處被向外探索的。只有鏈接到未發現的頂點的邊被探索了。當u全部的邊都被探索了,該算法回退到u被發現的地方去探索其餘的邊。這個過程持續到咱們發現了全部從原始頂點可以觸及的頂點。若是還留有任何其餘未被發現的頂點,咱們對新源頂點重複這個過程。重複該算法,直到圖中全部的頂點都被探索了。
觀察上述代碼能夠發現,time的值只能在圖的頂點數量的一到兩倍之間,而且d[u]<f[u],即發現時間的值比完成時間的值小。

拓撲排序

給定圖3,假定每一個頂點都是一個咱們須要去執行的任務。

圖3

clipboard.png

這是一個有向無環圖(DAG),即任務的執行是有順序的,例如,任務F不能在任務A以前執行,而且該圖沒有環。
當咱們須要編排一些任務或步驟的執行順序時,這稱爲拓撲排序。好比,當咱們在開發一個項目時,首先咱們得從客戶那裏獲得需求,接着開發客戶要求的東西,最後交付項目,但不能先交付項目再去收集需求。
拓撲排序只能應用於DAG。用深度優先搜索算法對圖3中的任務圖進行拓撲排序:

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中。圖4直觀地註明了各個頂點的發現時間/探索完成時間。

圖4

clipboard.png

那麼按照探索完成時間的倒序來排序獲得的拓撲排序爲:B - A - D - C - F - E須要注意的是,算法不一樣,獲得的拓撲排序可能不一樣,好比下面的拓撲排序也是可能的:A - B - C - D - F - E

相關文章
相關標籤/搜索