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

廣度優先搜索(BFS)

上一次已經提到,的遍歷通常有兩種算法,即廣度優先和深度優先。其中廣度優先搜索算法會從指定的第一個頂點開始遍歷圖,先訪問其全部的相鄰點,就像一次訪問圖的一層。換句話說,就是先寬後深地訪問頂點,如圖1。javascript

圖1java

clipboard.png

從頂點v開始的廣度優先搜索的步驟以下:算法

  1. 建立一個隊列Q。
  2. 將v標註爲被發現的(灰色) ,並將v入隊列Q。
  3. 若是Q非空,則運行如下步驟:segmentfault

    • 將u從隊列Q中取出
    • 將u標註爲被發現的(灰色)
    • 將u全部未被訪問過的鄰節點(白色)加入隊列
    • 將u標註爲已被探索的(黑色)

咱們用三種狀態來反映頂點的狀態:數組

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

所以在算法開始執行時,須要將全部頂點置爲白色;以下代碼所示(本文的全部代碼都是在中實現的Graph類中添加的),其中vertices保存着圖全部頂點的名字。數據結構

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;
        };
    }

廣度優先搜索算法的核心代碼以下,其中隊列Queue的實現參考基於JavaScript的數據結構 — 隊列的實現函數

this.bfs = function(v, callback){
        var color = initializeColor(), // 將全部頂點初始化爲白色
            queue = new Queue(); // 實例化隊列
        queue.enqueue(v); // 將起始頂點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'; // 該頂點訪問已經被徹底訪問,將其置爲黑
            callback && callback(u); // 用回調函數處理該節點(若是有)
        }
    };

使用BFS尋找最短路徑

因爲廣度優先算法是一層層往下遍歷的,即先訪問與起始頂點距離爲1的點,再訪問距離爲2的點,以此類推。所以,給定一個圖G和源頂點v, 要找出每個頂點u與v之間的最短距離(以邊的數量計算),能夠在上述代碼的基礎上作必定修改(修改的位置用空行隔開):this

// 獲取路徑信息
    this.pathData = function(v){
        var color = initializeColor(),
            queue = new Queue(),
            
            d = new Array(vertices.length).fill(0), // 用於保存起始頂點v到任意頂點u的距離
            pred = new Array(vertices.length).fill(null); // 用於保存v到u的路徑上u的上一級頂點(前溯點)
            
        queue.enqueue(v);
        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; // w到u的距離差是1
                    pred[w] = u; // w的上一級頂點是u
                    
                    queue.enqueue(w);
                }
            }
            color[u] = 'black';
        }
        
        return { // 返回保存的數據
            distances: d,
            predecessors: pred
        };
    };
    // 格式化輸出路徑信息
    this.printPathData = function (pathData) {
        var fromVertex = vertices[0]; // 獲取起始點
        for (var i=1; i<vertices.length; i++){
            var toVertex = vertices[i], // 要到達的頂點
                path = []; // 用於保存路徑
            // 從目標頂點一直回溯到起始頂點
            for (var v=toVertex; v!== fromVertex; v= pathData.predecessors[v]) {
                path.push(v); // 頂點添加進路徑
            }
            path.push(fromVertex); // 將起始頂點添加進路徑
            var s = path.pop();
            while (path.length > 0){
                s += ' - ' + path.pop(); // 從路徑數組倒序輸出頂點
            }
            console.log(s);
        }
    }

經過執行Graph.printPathData(Graph.pathData())便可輸出起始頂點到每個頂點的最短路徑。spa

其它最短路徑算法

對於加權圖的最短路徑,廣度優先算法可能並不合適。好比,Dijkstra’s算法能夠解決單源最短路徑問題。Bellman–Ford算法解決了邊權值爲負的單源最短路徑問題。A*搜索算法解決了求僅一對頂點間的最短路徑問題,它用經驗法則來加速搜索過程。Floyd–Warshall算法解決了求全部頂點對間的最短路徑這一問題。code

相關文章
相關標籤/搜索