用js來實現那些數據結構16(圖02-圖的遍歷)


  上一篇文章我們簡單介紹了一下什么是圖,以及用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);
//大家自己去看看打印的結果是什么。

  那么到這里,有關於圖的一部分內容基本上就都講解完畢了。可能大家覺得我有些偷懶,注釋寫的沒有以前那么詳細了啊。這是因為我覺得很多的內容前面都已經很詳細的說明過了。同樣的思路實在是沒必要翻來覆去的說來說去。所以反而到后面一些復雜的數據結構並沒有前面解釋的那么詳細。但是我覺得如果你一路看下來,這點東西絕壁難不倒你。

   

  最后,由於本人水平有限,能力與大神仍相差甚遠,若有錯誤或不明之處,還望大家不吝賜教指正。非常感謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM