Skip to content

Latest commit

 

History

History
282 lines (197 loc) · 10.4 KB

图.md

File metadata and controls

282 lines (197 loc) · 10.4 KB

数据结构 - 图

由一条边连接在一起的顶点称为: 相邻顶点, 比如,A 和 B 是相邻的,A 和 D 是相邻的,A 和 C 是相邻的,A 和 E 不是相邻的

一个顶点的度是其相邻顶点的数量。比如,A 和其他三个顶点相连接,因此,A 的度为 3; E 和其他两个顶点相连,因此,E 的度为 2。

创建图

声明图的骨架

function Graph() {
  var vertices = []; // 定义数组来存储图中所有顶点的名字
  var adjList = new Dictionary(); // 字典来存储邻接表
}

字典 将会使用顶点的名字作为键,邻接顶点列表作为值。vertices 数组和 adjList 字典两者都是我们 Graph 类的私有属性

接着,我们将实现两个方法: 一个用来向图中添加一个新的顶点(因为图实例化后是空的), 另外一个方法用来添加顶点之间的边。

// 这个方法接受顶点v作为参数。我们将该顶点添加到顶点列表中,并且在邻接表中,
// 设置顶点v作为键对应的字典值为一个空数组

this.addVertex = function(v) {
  vertices.push(v);
  adjList.set(v, []);
};
// 这个方法接受两个顶点作为参数。
// 首先,通过将w加入到v的邻接表中,我们添加了一条自顶点v到顶点w的边
// 例子采用无向图,所以还要加一条自定点w到v的边

this.addEdge = function(v, w) {
  adjList.get(v).push(w);
  adjList.get(w).push(v);
};

具体请看这个例子: createGraph.js


图的遍历

和树数据结构类似,我们可以访问图的所有节点。有两种算法可以对图进行遍历: 广度优先搜索(Breadth-First Search,BFS)深度优先搜索(Depth-First Search,DFS)

图遍历可以用来干哈?

  • 寻找特定的顶点

  • 寻找两个顶点之间的路径

  • 检查图是否连通

  • 检查图是否含有环

算法 数据结构 描述
深度优先搜索 DFS 通过将顶点存入栈中,顶点是沿着路径被探索的,存在新的相邻顶点就去访问
广度优先搜索 BFS 队列 通过将顶点存入队列中,最先入队列的顶点先被探索

最短路径?

首先先搞明白一点,广度优先搜索(BFS)能够让我们找到两样东西之间的最短路径,也就是说,最短路径问题,用到的算法叫做 “广度优先搜索”,用到的数据结构是 “队列”

但是最短路径问题,不是只有广度优先搜索算法,比如狄克斯特拉算法,也是解决最短路径问题

广度优先搜索用于在非加权图中查找最短路径

狄克斯特拉算法用于在加权图中查找最短路径

总的来说,BFS 多用于寻找最短路径的问题,DFS 多用于快速发现底部节点。

废话说的有点多了,下面继续看 DFS 和 BFS,当要标注已经访问过的顶点时,我们用三种颜色来反映它们的状态。

  • 白色:表示该顶点还没有被访问。

  • 灰色:表示该顶点被访问过,但并未被探索过。

  • 黑色:表示该顶点被访问过且被完全探索过。

这就是之前提到的务必访问每个顶点最多两次的原因

BFS

广度优先搜索算法会从指定的第一个顶点开始遍历图,先访问其所有的相邻点,就像一次访 问图的一层。换句话说,就是先宽后深地访问顶点,如下图所示:

以下是从顶点 v 开始的广度优先搜索算法所遵循的步骤。

  • (1) 创建一个队列 Q。

  • (2) 将 v 标注为被发现的(灰色),并将 v 入队列 Q。

  • (3) 如果 Q 非空,则运行以下步骤:

    • (a) 将 u 从 Q 中出队列;
    • (b) 将标注 u 为被发现的(灰色);
    • (c) 将 u 所有未被访问过的邻点(白色)入队列;
    • (d) 将 u 标注为已被探索的(黑色)。

看看该如何实现这个 简易版的 BFS 算法 ?

// 使用一个辅助数组 color。由于当算法开始执行时,所有的顶点颜色都是白色
var initializeColor = function() {
  var color = [];
  for (let i = 0; i < vertices.length; i++) {
    color[vertices[i]] = 'white';
  }
  return color;
};

this.bfs = function(v, callback) {
  var color = initializeColor(); // 1.初始化所有顶点
  var queue = new Queue(); // 2.初始化一个队列, 它将会存储待访问和待探索的顶点

  queue.enqueue(v); // 3.将起始点 v 入队列

  while (!queue.isEmpty()) {
    // 4. 队列非空
    var u = queue.dequeue(); // 5. 从队列中移除一个顶点
    neighbors = adjList.get(u); // 6. 从字典中取得它的邻接表
    color[u] = 'grey'; // 7. 将该顶点置为灰色,表示被访问过,但未被探索过

    for (let j = 0; j < neighbors.length; j++) {
      // 8. 对于出队的这个顶点,遍历它的相邻点
      var w = neighbors[j]; // 9. 取得这个点
      if (color[w] === 'white') {
        // 10. 如果这个点没有被访问过
        color[w] = 'grey'; // 11. 标注为发现了这点,但是还没有探索
        queue.enqueue(w); // 12. 将这个顶点加入到队列中
      }
    }
    color[u] = 'black'; // 13. 标注出队点这个顶点已经访问并探索完毕,下次不应该访问
    if (callback) {
      callback(u); // 14. 接受一个回调
    }
  }
};

OK,上边的只是一个 BFS 算法的工作原理,但是可以用该算法做更多事情,而不只是输出被访问顶点的顺序。例如,考虑如何来解决下面这个问题。

给定一个图 G 和源顶点 v,找出对每个顶点 u,u 和 v 之间最短路径的距离(以边的数量计)。

对于给定顶点 v,广度优先算法会访问所有与其距离为 1 的顶点,接着是距离为 2 的顶点, 以此类推。所以,可以用广度优先算法来解这个问题。我们可以修改 bfs 方法以返回给我们一 些信息:

  • 从 v 到 u 的距离 d[u]

  • 前溯点 pred[u],用来推导出从 v 到其他每个顶点 u 的最短路径

改进版的 BFS 算法

this.updateBFS = function() {
  var color = initializeColor(); // 1.初始化所有顶点
  var queue = new Queue(); // 2.初始化一个队列, 它将会存储待访问和待探索的顶点
  var d = []; // 3. 声明数组来表示距离
  var pred = []; // 4. 声明pred数组来表示前溯点

  queue.enqueue(v); // 5.将起始点 v 入队列

  for (let i = 0; i < vertices.length; i++) {
    d[vertices[i]] = 0; // 6.图中的每一个顶点,用0来初始化数组d
    pred[vertices[i]] = null; // 7. 用null来初始化数组pred
  }

  while (!queue.isEmpty()) {
    // 8. 队列非空
    var u = queue.dequeue(); // 9. 从队列中移除一个顶点
    neighbors = adjList.get(u); // 10. 从字典中取得它的邻接表
    color[u] = 'grey'; // 11. 将该顶点置为灰色,表示被访问过,但未被探索过

    for (let j = 0; j < neighbors.length; j++) {
      // 12. 对于出队的这个顶点,遍历它的相邻点
      var w = neighbors[j]; // 13. 取得这个点
      if (color[w] === 'white') {
        // 14. 如果这个点没有被访问过
        color[w] = 'grey'; // 15. 标注为发现了这点,但是还没有探索
        d[w] = d[u] + 1; // 16. 通过给d[u]加1来设置两点之间的距离
        pred[w] = u; // 17. 发现顶点u的邻点w时,则设置w的前溯点值为u
        queue.enqueue(w); // 18. 将这个顶点加入到队列中
      }
    }
    color[u] = 'black'; // 19. 标注出队点这个顶点已经访问并探索完毕,下次不应该访问

    // 20. 方法最后返回了一个包含d和pred的对象
    return {
      distances: d,
      predecessors: pred
    };
  }
};

DFS

深度优先搜索算法将会从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶 点被访问了,接着原路回退并探索下一条路径。换句话说,它是先深度后广度地访问顶点,如下图所示:

深度优先搜索算法不需要一个源顶点。在深度优先搜索算法中,若图中顶点 v 未访问,则访问该顶点 v。

要访问顶点 v,照如下步骤做。

  • (1) 标注 v 为被发现的(灰色)。

  • (2) 对于 v 的所有未访问的邻点 w:

    • (a) 访问顶点 w。
  • (3) 标注 v 为已被探索的(黑色)。

如你所见,深度优先搜索的步骤是递归的,这意味着深度优先搜索算法使用栈来存储函数调用(由递归调用所创建的栈)。

看看该如何实现这个 简易版的 DFS 算法 ?

this.dfs = function(callback) {
  var color = initializeColor(); // 1.初始化所有顶点

  for (let i = 0; i < vertices.length; i++) {
    // 2. 对每一个未被访问过的顶点
    if (color[vertices[i]] === 'white') {
      dfsVisit(vertices[i], color, callback); // 3. 调用私有递归函数 dfsVisit
    }
  }
};

this.dfsVisit = function(u, color, callback) {
  color[u] = 'grey'; // 4. 置为被发现,但是还未探索
  if (callback) {
    callback(u);
  }

  var neighbors = adjList.get(u); // 5. 取得顶点 u 所有邻点的列表

  for (let j = 0; j < neighbors.length; j++) {
    // 6. 遍历所有邻点
    var w = neighbors[j]; // 7. 顶点u的每一个未被访问过的邻点w
    if (color[w] === 'white') {
      dfsVisit(w, color, callback); // 8. 调用dfsVisit函数,添加顶点w入栈
    }
  }
  color[u] = 'blank';
};

如以下图所示 :


我强烈推荐,先去看这篇 搜索思想-DFS & BFS 文章,然后接着去 youbute 看这个视频 BFS、DFS 算法原理及 JS 实现!!!

相关文章