學習javascript資料結構與演算法(六)——圖

wengjq發表於2019-02-28

前言

本文是博主深感演算法方面的不足,作的一系列讀書筆記和原始碼分析。
原文地址:學習javascript資料結構與演算法(六)——圖,覺得有用的話可以給個star,謝謝啦。
作者:wengjq

1、 圖

圖是網路結構的抽象模型。圖是一組由邊連線的節點,任何二元關係都可以用圖來表示。

1.1、圖的相關概念

一個圖G = (V,E)由以下元素組成。

  • V:一組頂點
  • E:一組邊,連線V中的頂點

下圖表示一個圖:

學習javascript資料結構與演算法(六)——圖

由一條邊連線在一起的頂點稱為相鄰頂點。比如上圖的A和B是相鄰的,A和D是相鄰的,A和C是相鄰的,A和E不是相鄰的。一個頂點的度是其相鄰頂點的數量。比如,A和其他三個頂點相連線,因此,A的度為3;E和其他兩個頂點相連,因此E的度為2。路徑是頂點v1,v2,…,vk的一個連續序列,其中v[i]和v[i+1]是相鄰的。以上的圖為例,其中的路徑有ABEI和ACDG。

1.2、圖的表示

圖最常見的實現是鄰接矩陣。每個節點都和一個整數相關聯,該整數作為陣列的索引。這裡不作討論。另一種圖的表示方式是一種叫做鄰接表的動態資料結構。鄰接表由圖中每個頂點的相鄰頂點列表所組成。我們可以用陣列,連結串列,甚至是雜湊表或是字典來表示相鄰頂點列表。下面的示意圖展示了鄰接表的資料結構。我們後面也會用程式碼示例這種資料結構。

學習javascript資料結構與演算法(六)——圖
鄰接表

示例程式碼如下:

function Graph() {
    var vertices = []; //儲存圖中所有的頂點名字
    var adjList = new Dictionary();//用之前的一個字典來儲存鄰接表
    this.addVertex = function(v){ //新增頂點
        vertices.push(v);
        adjList.set(v, []); //頂點為鍵,字典值為空陣列
    };
    this.addEdge = function(v, w){ //新增邊
        adjList.get(v).push(w); //基於有向圖
        adjList.get(w).push(v); //基於無向圖
    };
    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 += `
`;
        }
        return s;
    };
    var initializeColor = function(){
        var color = [];
        for (var i=0; i<vertices.length; i++){
            color[vertices[i]] = `white`;
        }
        return color;
    };
}  
//測試
var graph = new Graph();
var myVertices = [`A`,`B`,`C`,`D`,`E`,`F`,`G`,`H`,`I`];
for (var i=0; i<myVertices.length; i++){
    graph.addVertex(myVertices[i]);
}
graph.addEdge(`A`, `B`);
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`);
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 複製程式碼

1.3、圖的遍歷

和樹的資料結構類似,我們可以訪問圖的所有節點。有兩種演算法可以對圖進行遍歷:廣度優先搜尋(Breadth-First Search,BFS)和深度優先搜尋(Depth-First Search,DFS)。圖遍歷可以用來尋找特定的頂點或尋找兩個頂點之間的路徑,檢查圖是否連通,檢查圖是否含有環等。

圖遍歷演算法的思想是必須追蹤每個第一次訪問的節點,並且追蹤有哪些節點還沒有被完全探索。對於兩種圖遍歷演算法,都需要明確指出第一個被訪問的頂點。完全探索一個頂點要求我們檢視該頂點的每一條邊。對應每一條邊所連線的沒有被訪問過的頂點,將其標註為被發現的,並將其加進待訪問頂點列表中。

為了保證演算法的效率,務必訪問每個頂點至多兩次。連通圖中每條邊和頂點都會被訪問到。當要標註已經訪問過的頂點時,我們用三種顏色來反映它們的狀態:

  • 白色:表示該頂點還沒有被訪問。
  • 灰色:表示該頂點被訪問過,但並未被探索過。
  • 黑色:表示該頂點被訪問過且被完全搜尋過。

1.3.1、廣度優先搜尋(BFS)

廣度優先搜尋演算法會從指定的第一個頂點開始遍歷圖,先訪問其所有的相鄰點,就像一次訪問圖的一層。換句話說,就是先寬後深的訪問頂點。以下是從頂點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`; //初始化所有的頂點都是白色
      }
      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);
          }
      }
  };
      //測試如下:
     function printNode(value){
         console.log(`Visited vertex: ` + value);
     }
     graph.bfs(myVertices[0], printNode);
     結果如下:
     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複製程式碼

1.3.2、深度優先搜尋(BFS)

深度優先搜尋演算法將會是從第一個指定的頂點開始遍歷圖,沿著路徑直到這條路徑最後一個頂點被訪問了,接著原路回退並探索下一條路徑。換句話說,它是先深度後廣度地訪問頂點。深度優先搜尋演算法不需要一個源頂點。要訪問頂點v,照如下的步驟做:

  • (1)標註v為被發現的(灰色)。
  • (2)對應v的所有未訪問的鄰點w。
        (a)訪問頂點w。複製程式碼
  • (3)標註v為已被探索的(黑色)。

如你所見,深度優先搜尋的步驟是遞迴的,這意味著深度優先搜尋演算法使用棧來儲存函式呼叫(由遞迴呼叫所建立的棧)。示例程式碼如下:

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);
    }
    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); //新增頂點w入棧
        }
    }
    color[u] = `black`;
};
//測試如下:
function printNode(value){
   console.log(`Visited vertex: ` + value);
}
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複製程式碼

本文對你有幫助?歡迎掃碼加入前端學習小組微信群:

學習javascript資料結構與演算法(六)——圖

相關文章