相邻关系列表
图可以用不同的方式表示。这里我们描述一种方式,它被称为邻接列表。邻接列表本质上是一个列举式列表,左边是节点,右边列出它所连接的所有其他节点。下面是一个邻接列表的表示。
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));