图与DFS,BFS

136 阅读7分钟

相邻关系列表

图可以用不同的方式表示。这里我们描述一种方式,它被称为邻接列表。邻接列表本质上是一个列举式列表,左边是节点,右边列出它所连接的所有其他节点。下面是一个邻接列表的表示。

Node1: Node2, Node3
Node2: Node1
Node3: Node1

以上是一个无向图,因为Node1与Node2和Node3相连,而且这个信息与Node2和Node3显示的连接是一致的。有向图的邻接列表将意味着列表的每一行都显示了方向。如果上面是有向的,那么Node2: Node1将意味着有向边从Node2指向Node1。我们可以把上面的无向图放在一个JavaScript对象中,把它表示为一个邻接列表。

var undirectedG = {
  Node1: ["Node2", "Node3"],
  Node2: ["Node1"]。
  Node3: ["Node1"]
};

这也可以更简单地表示为一个数组,其中节点只是有数字而不是字符串标签。

var undirectedGArr = [
  [1, 2], // Node1
  [0], // Node2
  [0] // Node3
];

创建一个社交网络,作为一个无向图,有4个节点/人,分别是James、Jill、Jenny和Jeff。詹姆斯和杰夫、吉尔和珍妮、以及杰夫和珍妮之间有边/关系。

var undirectedAdjList = {
  James: ["Jeff"],
  Jill: ["Jenny"],
  Jenny: ["Jill", "Jeff"],
  Jeff: ["Jenny", "James"]
};

邻接矩阵

另一种表示图形的方法是把它放在一个邻接矩阵中。邻接矩阵是一个二维(2D)数组,其中每个嵌套数组的元素数量与外层数组相同。换句话说,它是一个数字矩阵或网格,其中的数字代表边。

注意:矩阵的顶部和左侧的数字只是节点的标签。在矩阵中,1意味着代表行和列的顶点(节点)之间存在一条边。最后,零表示没有边或关系。

    1 2 3
  \------
1 | 0 1 1
2 | 1 0 0
3 | 1 0 0

上面是一个非常简单的无向图,你有三个节点,其中第一个节点与第二个和第三个节点相连。下面是同样事情的一个JavaScript实现。

var adjMat = [
  [0, 1, 1],
  [1, 0, 0],
  [1, 0, 0]
];

与邻接列表不同,矩阵的每一 "行 "都必须有与图中节点相同数量的元素。这里我们有一个三乘三的矩阵,这意味着我们的图中有三个节点。一个有向图看起来也差不多。下面是一个图,第一个节点有一条指向第二个节点的边,然后第二个节点有一条指向第三个节点的边。

var adjMatDirected = [
  [0, 1, 0],
  [0, 0, 1],
  [0, 0, 0]
];

图也可以在其边上有权重。到目前为止,我们有未加权的边,其中只有边的存在和缺乏是二进制的(0或1)。根据你的应用,你可以有不同的权重。

创建一个有五个节点的无向图的相邻矩阵。这个矩阵应该是一个多维数组。这五个节点在第一和第四节点、第一和第三节点、第三和第五节点、第四和第五节点之间有关系。所有边的权重都是一。

const adjMatUndirected = [
  // 1 2 3 4 5
  [0, 0, 1, 1, 0], // 1
  [0, 0, 0, 0, 0], // 2
  [1, 0, 0, 0, 1], // 3
  [1, 0, 0, 0, 1], // 4
  [0, 0, 1, 1, 0]  // 5
];

发生率矩阵

另一种表示图形的方法是把它放在一个发生率矩阵中。

入射矩阵是一个二维(2D)阵列。一般来说,一个发生矩阵在其两个维度之间将两个不同类别的对象联系起来。这种矩阵类似于邻接矩阵。然而,这里的行和列意味着别的东西。

在图中,我们有边和结点。这些将是我们的 "两个不同类别的对象"。这个矩阵的行是节点,列是边。这意味着我们可以有不均匀数量的行和列。

每一列将代表一条独特的边。另外,每条边都连接着两个节点。为了表明两个节点之间有一条边,你将在某一列的两行中放一个1。下面是一个3个节点的图形,节点1和节点3之间有一条边。

1
---
1 | 1
2 | 0
3 | 1

下面是一个有4条边和4个节点的入射矩阵的例子。记住,列是边,行是节点本身。

1 2 3 4
--------
1 | 0 1 1 1
2 | 1 1 0 0
3 | 1 0 0 1
4 | 0 0 1 0

下面是同样事情的一个JavaScript实现。

var incMat = [
  [0, 1, 1, 1],
  [1, 1, 0, 0],
  [1, 0, 0, 1],
  [0, 0, 1, 0]
];

要制作一个有向图,用-1表示离开某个节点的边,用1表示进入某个节点的边。

var incMatDirected = [
  [ 0, -1, 1, -1],
  [-1, 1, 0, 0],
  [ 1, 0, 0, 1],
  [ 0, 0, -1, 0]
];

图也可以在其边上有权重。到目前为止,我们有未加权的边,其中只有边的存在和缺乏是二进制的(0或1)。根据你的应用,你可以有不同的权重。不同的权重表示为大于1的数字。

创建一个有五个节点和四条边的无向图的发生矩阵。这个矩阵应该是一个多维数组。

这五个节点有以下关系。第一条边是在第一个和第二个节点之间。第二条边是在第二个和第三个节点之间。第三条边是在第三和第五节点之间。第四条边是在第四个节点和第二个节点之间。所有边的权重都是1,边的顺序很重要。

const incMatUndirected = [
  // 1 2 3 4
  [1, 0, 0, 0], // 1
  [1, 1, 0, 1], // 2
  [0, 1, 1, 0], // 3
  [0, 0, 0, 1], // 4
  [0, 0, 1, 0]  // 5
];

广度优先搜索

到目前为止,我们已经学会了创建图形表示法的不同方法。现在呢?一个自然的问题是,图中任何两个节点之间的距离是多少?进入图的遍历算法。

遍历算法是遍历或访问图中节点的算法。遍历算法的一种类型是 "广度优先 "搜索算法。

这种算法从一个节点开始,访问其所有相距一条边的邻居。然后继续访问它们的每一个邻居,以此类推,直到所有的节点都被到达。

一个有助于实现广度优先搜索算法的重要数据结构是队列。这是一个数组,你可以将元素添加到一端,并从另一端移除元素。这也被称为FIFO或先入先出的数据结构。

从视觉上看,这就是该算法正在做的事情。广度优先搜索算法在树上移动

灰色阴影代表一个节点被添加到队列中,黑色阴影代表一个节点被从队列中移除。请看,每当一个节点从队列中被移除(节点变成黑色),它们的所有邻居都会被添加到队列中(节点变成灰色)。

为了实现这个算法,你需要输入一个图的结构和一个你想开始的节点。

首先,你要注意离起始节点的距离,或者说离起始节点的边数。你想用一些大的数字来开始你的距离,比如Infinity。这可以防止当一个节点可能无法从你的起始节点到达时的计数问题。接下来,你要从起始节点到它的邻居。这些邻居有一条边的距离,在这一点上,你应该在你所记录的距离上增加一个单位的距离。

编写一个函数bfs(),它接收一个邻接矩阵图(一个二维数组)和一个节点标签根作为参数。节点标签将只是0和n - 1之间的节点的整数值,其中n是图中节点的总数。

你的函数将输出一个JavaScript对象的键值对,包括节点和它与根的距离。如果节点无法到达,它的距离应该是无限大。

function bfs(graph, root) {
  // 返回的距离对象
  var nodesLen = {};
  // 将所有距离设置为无穷大
  for (var i = 0; i < graph.length; i++) {
    nodesLen[i] = Infinity;
  }
  nodesLen[root] = 0; // ...除了根节点之外
  var queue = [root]; // 跟踪要访问的节点
  var current; // 当前正在遍历的节点
  // 继续下去,直到没有更多的节点需要遍历为止
  while (queue.length !==0) {
    current = queue.shift()。
    // 从当前节点获取相邻的节点
    var curConnected = graph[current]; // 从当前节点获取边的层数
    var neighborIdx = []; // 有边的节点列表
    var idx = curConnected.indexOf(1); // 获得第一个边的连接
    while (idx !==-1) {
      neighborIdx.push(idx); // 添加到邻居的列表中
      idx = curConnected.indexOf(1, idx + 1); // 继续搜索
    }
    // 循环浏览邻居并获得长度
    for (var j = 0; j < neighborIdx.length; j++) {
      // 递增所遍历的节点的距离
      if (nodesLen[neigherIdx[j]] === Infinity) {
        nodesLen[neigherIdx[j]] = nodesLen[current] + 1。
        queue.push(neigherIdx[j]); // 将新的邻居加入队列。
      }
    }
  }
  return nodesLen;
}

深度优先搜索

与广度优先搜索类似,这里我们将学习另一种图的遍历算法,即深度优先搜索。

广度优先搜索是在远离源节点的情况下逐步搜索边的长度,而深度优先搜索则首先沿着一条边的路径尽可能地往下搜索。

一旦它到达路径的一端,搜索将回溯到最后一个有未访问边缘路径的节点并继续搜索。

下面的动画显示了该算法是如何工作的。该算法从顶部节点开始,按编号顺序访问节点。

请注意,与广度优先搜索不同的是,每次访问一个节点时,它并不访问其所有的邻居。相反,它首先访问它的一个邻居,然后沿着这个路径继续下去,直到该路径上没有更多的节点需要访问。

为了实现这个算法,你要使用一个堆栈。栈是一个数组,最后加入的元素是第一个被移除的。这也被称为 "后进先出 "的数据结构。栈在深度优先搜索算法中很有帮助,因为当我们向栈中添加邻居时,我们要先访问最近添加的邻居,并从栈中删除它们。

这个算法的一个简单输出是一个从给定节点可到达的节点列表。因此,你也要跟踪你所访问的节点。

编写一个函数dfs(),将一个无定向的邻接矩阵图和一个节点标签根作为参数。节点标签将只是0和n - 1之间的节点的数值,其中n是图中节点的总数。

你的函数应该输出一个数组,包含从根部可到达的所有节点。

function dfs(graph, root) {
  var stack = [];
  var tempV;
  var visited = [];
  var tempVNeighbors = [];
  stack.push(root);
  while (stack.length > 0) {
    tempV = stack.pop();
    if (visited.indexOf(tempV) == -1) {
      visited.push(tempV);
      tempVNeighbors = graph[tempV];
      for (var i = 0; i < tempVNeighbors.length; i++) {
        if (tempVNeighbors[i] == 1) {
          stack.push(i);
        }
      }
    }
  }
  return visited;
}

var exDFSGraph = [
  [0, 1, 0, 0],
  [1, 0, 1, 0],
  [0, 1, 0, 1],
  [0, 0, 1, 0]
];
console.log(dfs(exDFSGraph, 3));