前言
这个图来自王道考研的
数据结构
视频课程中的截图。B 站可搜王道考研数据结构
之前分享了 BFS 和 Dijkstra 算法,可以从上图了解到, BFS 和 Dijkstra 算法是单源最短路径。也就是说,只能计算从一个节点出发,到其他节点的最短路径。
而 Floyd 可以计算任意节点的最短路径。是不是很厉害,快来看看是怎么回事
Dijkstra:Dijkstra寻找最短路径-JS简易版 - 掘金
Floyd算法的核心思想是动态规划(先做一部分,然后再做一部分)。
具体来说,它通过一个二维数组dist来存储任意两个顶点之间的最短路径长度,其中dist[i][j]表示顶点i到顶点j的最短路径长度。初始时,dist[i][j]的值等于顶点i到顶点j的边的权值,即如果顶点i和顶点j之间没有边,则dist[i][j]=∞。
Floyd算法的主要步骤如下:
- 初始化dist数组,将每条边的权值存储在dist数组中。
- 对于所有顶点k,然后遍历所有顶点i和顶点j,试图找到顶点 k 当作中间节点后,能够缩短顶点 i 和顶点 j 的路径,即 dist[i][k] + dist[k][j] < dist[i][j],如果找到了,就更新dist[i][j]的值,即顶点 i 和顶点 j 的路径的值
- 重复步骤2,直到遍历完所有顶点k。
过程很简单,下面来用代码实现它
准备数据
这个图来自王道考研的数据结构
视频课程中的截图。B 站可搜王道考研数据结构
【精准空降】➡️ 6.4_4_最短路径问题_Floyd算法
代码会根据这张有向图来生成邻接矩阵,用以实现下面的 floyd 编写
javascript
const nodeCount = 5; // 定义节点数量
/**@type {number[]} */
let graphNodes = Array(nodeCount) // 创建一个包含节点索引的数组
.fill(0) // 用0填充数组
.map((item, index) => index); // 将索引添加到数组中
/**@type {number[][]} */
const graphEdges = { // 定义一个包含边及其权值的二维数组
0: [ // 节点0的边
[2, 1], // 边0: 节点0 -> 节点2,权值为1
[4, 10], // 边1: 节点0 -> 节点4,权值为10
],
1: [ // 节点1的边
[3, 1], // 边0: 节点1 -> 节点3,权值为1
[4, 5], // 边1: 节点1 -> 节点4,权值为5
],
2: [ // 节点2的边
[1, 1], // 边0: 节点2 -> 节点1,权值为1
[4, 7], // 边1: 节点2 -> 节点4,权值为7
],
3: [[4, 1]], // 节点3的边
};
/**
*
* @param {number[]} graphNodes
* @param {number[][]} graphEdges
* @returns {nodeType[]}
*/
const generateGraph = (graphNodes, graphEdges) => { // 定义一个名为generateGraph的函数
/** @type {number[][]} */
const graph = Array(nodeCount) // 创建一个大小为节点数量的二维数组
.fill(0) // 用0填充数组
.map((item, index) =>
Array(nodeCount) // 创建一个大小为节点数量的二维数组
.fill(999) // 用999填充数组
.map((item, indexInner) => { // 遍历二维数组中的每个元素
if (indexInner === index) return 0; // 如果索引相等,返回0
return item; // 否则返回原来的值
})
);
Object.entries(graphEdges).map(([value, edges]) => { // 遍历graphEdges中的每个键值对
edges.map(([edge, power]) => { // 遍历键值对中的每个边及其权值
graph[value][edge] = power; // 将边及其权值映射到graph数组中
});
});
return graph; // 返回graph数组
};
const graph = generateGraph(graphNodes, graphEdges); // 调用generateGraph函数,传入graphNodes和graphEdges,得到graph数组
这段代码的目的是根据graphNodes和graphEdges创建一个有向图的邻接矩阵。
首先定义了一个有向图的节点数量,然后定义了一个包含节点索引的数组graphNodes,以及一个包含边及其权值的二维数组graphEdges。
在generateGraph函数中,首先使用Array.fill()方法创建一个大小为nodeCount的二维数组graph,并用999填充每个元素。然后使用Object.entries()方法遍历graphEdges中的每个键值对,将每个键值对中的边及其权值映射到graph数组中。
用 999 表示非常大的权值,没有边可以大过这个权值,也即表示两个点不可达
最后,generateGraph函数返回graph数组,该数组表示有向图的邻接矩阵。
Floyd
javascript
const path = Array(nodeCount) // 定义一个大小为nodeCount的二维数组path,用于记录路径
.fill(0) // 用0填充数组
.map((item) => Array(nodeCount).fill(-1)); // 用-1填充数组
const floyd = (graph) => { // 定义一个名为floyd的函数,接受一个参数graph,表示一个邻接矩阵表示的图
for (let midNode = 0; midNode < nodeCount; midNode++) { // 遍历所有中间节点
for (let from = 0; from < nodeCount; from++) { // 遍历所有可能的起点
for (let to = 0; to < nodeCount; to++) { // 遍历所有可能的终点
if (graph[from][to] > graph[from][midNode] + graph[midNode][to]) { // 如果通过中间节点从起点到终点的路径比当前路径更短
graph[from][to] = graph[from][midNode] + graph[midNode][to]; // 更新路径长度
path[from][to] = midNode; // 将中间节点记录为路径的一部分
}
}
}
}
};
这段代码定义了一个名为floyd的函数,该函数接受一个参数graph,表示一个邻接矩阵表示的图。该函数用来实现 Floyd 算法
参数 graph 存储了任意两个节点之间的邻接距离,在经过 floyd 函数之后,graph 中,就会存储任意两个点的最短距离
在floyd函数中,首先使用Array.fill()方法创建一个大小为nodeCount的二维数组path,并用-1填充数组。path数组将用于记录任意两个顶点之间的中间节点。
path[0][3] = -1
就表示 0 和 3 两个节点之间没有中间节点,直达。若path[0][3] = 2
,则表示0 和 3 两个节点之间有一个中间节点 2。但不是只有节点 2,可能还有节点 1, 这个需要注意。不太明白,没关系,看看下面的输出结果就可以明白这句话了
接下来,使用for循环遍历所有中间节点midNode。对于每个中间节点,使用另一个for循环遍历所有可能的起点from和终点to。如果通过中间节点midNode从顶点from到顶点to的路径比当前路径更短,则更新路径长度,并将中间节点midNode记录为路径的一部分。
最后,floyd函数返回更新后的邻接矩阵graph和路径数组path。
执行函数
javascript
floyd(graph);
console.log(graph, path);
// [
// [ 0, 2, 1, 3, 4 ],
// [ 999, 0, 999, 1, 2 ],
// [ 999, 1, 0, 2, 3 ],
// [ 999, 999, 999, 0, 1 ],
// [ 999, 999, 999, 999, 0 ]
// ]
// [
// [ -1, 2, -1, 2, 3 ],
// [ -1, -1, -1, -1, 3 ],
// [ -1, -1, -1, 1, 3 ],
// [ -1, -1, -1, -1, -1 ],
// [ -1, -1, -1, -1, -1 ]
// ]
邻接矩阵graph 存放着无向图中任意两个顶点之间的最短路径长度;在path数组中记录了路径信息
详细来看看输出的结果:
graph:
第一行表示顶点0的邻接关系,
- 顶点 0 到顶点 0 的边权为 0,表示自己到自己;
- 其中顶点0到顶点 1 的边权为2,表示最短距离就是 2;
- 顶点0到顶点 2 的边权为1,表示最短距离就是 1;
- 顶点0到顶点3的边权为3,表示最短距离就是 3;
- 顶点0到顶点4的边权为4,表示最短距离就是 4
第二行表示顶点1的邻接关系,
- 其中顶点1到顶点 0 的边权为999,表示不可达;
- 顶点1到顶点 1 的边权为 0,表示自己到自己;
- 顶点1到顶点 2 的边权为999,表示不可达;
- 顶点1到顶点3的边权为999,表示不可达;
- 顶点1到顶点4的边权为2,表示最短距离就是 2
以此类推
path:
第一行表示从顶点0 出发的信息:
path[0][0] == -1
: 到顶点 0 的路径,中间不经过任何节点。path[0][1] == 2
:到顶点 1 的路径,中间经过节点 1。path[0][2] == -1
:到顶点 2 的路径,中间不经过任何节点。path[0][3] == 2
:到顶点 3 的路径,中间经过节点 2。path[0][4] == 3
:到顶点 4 的路径,中间经过节点 3。
以此类推
好,到这里,相信大家已经懂得如何看这两个数组的输出了。下面我们来看看节点 0 到节点 4 的最短路径,以及中间经过了哪些节点
- graph[0][4] == 4, 表示节点 0 到节点 4 最短距离就是 4
- path[0][4] == 3, 表示节点 0 到节点 4 中间经过 3。 再看看节点 0 到节点 3,以及节点 3 到节点 4 中间是否节点
- path[0][3] == 2, path[3][4] == -1 ,表示节点 3 到节点 4 中间没有节点,节点 0 到节点 3 中间经过节点 2. 再往下看看节点 0 到节点 2,以及节点 2 到节点 3 中间是否节点
- ...
大概过程就是这样,我就不写下去了。人工看输出结果太麻烦了,还是用代码来读吧
打印结果
javascript
const getPath = (from, to) => {
// 如果顶点from到顶点to的路径长度为999,则返回一个空数组,表示没有路径
if (graph[from][to] === 999) return [];
// 如果顶点from到顶点to的路径中间有其他节点,则继续向下寻找路径
if (path[from][to] !== -1) {
return [...getPath(from, path[from][to]), ...getPath(path[from][to], to).slice(1)];
}
// 如果顶点from到顶点to的路径没有其他节点,则返回一个包含from和to的数组,表示这两个顶点之间的最短路径就是它们之间的直接路径
return [from, to];
};
这段代码定义了一个名为getPath的函数,该函数接受两个参数from和to,表示要查找的两个顶点。函数的返回值是一个数组,表示从顶点from到顶点to的最短路径。
代码很简单,就不解释了
直接看结果
javascript
// 打印从顶点0到顶点4的路径
console.log(getPath(0, 4));
//[ 0, 2, 1, 3, 4 ]
// 打印从顶点3到顶点0的路径
console.log(getPath(3, 0));
//[]
可以对照下面这张有向图来看打印结果是否正确:
完整代码
javascript
const nodeCount = 5;
/**@type {number[]} */
let graphNodes = Array(nodeCount)
.fill(0)
.map((item, index) => index);
/**@type {number[][]} */
const graphEdges = {
0: [
[2, 1],
[4, 10],
],
1: [
[3, 1],
[4, 5],
],
2: [
[1, 1],
[4, 7],
],
3: [[4, 1]],
};
/**
*
* @param {number[]} graphNodes
* @param {number[][]} graphEdges
* @returns {nodeType[]}
*/
const generateGraph = (graphNodes, graphEdges) => {
/** @type {number[][]} */
const graph = Array(nodeCount)
.fill(0)
.map((item, index) =>
Array(nodeCount)
.fill(999)
.map((item, indexInner) => {
if (indexInner === index) return 0;
return item;
})
);
Object.entries(graphEdges).map(([value, edges]) => {
edges.map(([edge, power]) => {
graph[value][edge] = power;
});
});
return graph;
};
const graph = generateGraph(graphNodes, graphEdges);
const path = Array(nodeCount)
.fill(0)
.map((item) => Array(nodeCount).fill(-1));
const floyd = (graph) => {
for (let midNode = 0; midNode < nodeCount; midNode++) {
for (let from = 0; from < nodeCount; from++) {
for (let to = 0; to < nodeCount; to++) {
if (graph[from][to] > graph[from][midNode] + graph[midNode][to]) {
graph[from][to] = graph[from][midNode] + graph[midNode][to];
path[from][to] = midNode;
}
}
}
}
};
floyd(graph);
console.log(graph, path);
// [
// [ 0, 2, 1, 3, 4 ],
// [ 999, 0, 999, 1, 2 ],
// [ 999, 1, 0, 2, 3 ],
// [ 999, 999, 999, 0, 1 ],
// [ 999, 999, 999, 999, 0 ]
// ]
// [
// [ -1, 2, -1, 2, 3 ],
// [ -1, -1, -1, -1, 3 ],
// [ -1, -1, -1, 1, 3 ],
// [ -1, -1, -1, -1, -1 ],
// [ -1, -1, -1, -1, -1 ]
// ]
const getPath = (from, to) => {
// there are no path
if (graph[from][to] === 999) return [];
if (path[from][to] !== -1) {
return [...getPath(from, path[from][to]), ...getPath(path[from][to], to).slice(1)];
}
return [from, to];
};
console.log(getPath(0, 4));
console.log(getPath(3, 0));
代码可以直接 copy 到本地运行,学习,或测试
总结
篇文章分享了 JS 代码实现 floyd 算法。代码清晰,注释详细,是一篇不可多的的好文章啊
之前在大学学的时候,不明觉厉,现在用代码实现一遍,发现还是很简单的,而且代码实现能够使理解更深刻。
纸上得来终觉浅,觉知此事要 coding。
我还分享了文章提到的两个算法,感兴趣可以去看看呀,都很简单:
Dijkstra:Dijkstra寻找最短路径-JS简易版 - 掘金