🥳每日一练-Floyd算法求最短路径-JS简易版 - 掘金

前言

这个图来自王道考研的数据结构视频课程中的截图。B 站可搜王道考研数据结构

之前分享了 BFS 和 Dijkstra 算法,可以从上图了解到, BFS 和 Dijkstra 算法是单源最短路径。也就是说,只能计算从一个节点出发,到其他节点的最短路径。

而 Floyd 可以计算任意节点的最短路径。是不是很厉害,快来看看是怎么回事

BFS:BFS算法求最短路径-JS简易版 - 掘金

Dijkstra:Dijkstra寻找最短路径-JS简易版 - 掘金

Floyd算法的核心思想是动态规划(先做一部分,然后再做一部分)。

具体来说,它通过一个二维数组dist来存储任意两个顶点之间的最短路径长度,其中dist[i][j]表示顶点i到顶点j的最短路径长度。初始时,dist[i][j]的值等于顶点i到顶点j的边的权值,即如果顶点i和顶点j之间没有边,则dist[i][j]=∞。

Floyd算法的主要步骤如下:

  1. 初始化dist数组,将每条边的权值存储在dist数组中。
  2. 对于所有顶点k,然后遍历所有顶点i和顶点j,试图找到顶点 k 当作中间节点后,能够缩短顶点 i 和顶点 j 的路径,即 dist[i][k] + dist[k][j] < dist[i][j],如果找到了,就更新dist[i][j]的值,即顶点 i 和顶点 j 的路径的值
  3. 重复步骤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。

我还分享了文章提到的两个算法,感兴趣可以去看看呀,都很简单:

BFS:BFS算法求最短路径-JS简易版 - 掘金

Dijkstra:Dijkstra寻找最短路径-JS简易版 - 掘金

相关推荐
緈福的街口4 分钟前
【leetcode】584. 寻找用户推荐人
算法·leetcode·职场和发展
future14128 分钟前
游戏开发日记
数据结构·学习·c#
今天背单词了吗98010 分钟前
算法学习笔记:17.蒙特卡洛算法 ——从原理到实战,涵盖 LeetCode 与考研 408 例题
java·笔记·考研·算法·蒙特卡洛算法
Maybyy31 分钟前
力扣242.有效的字母异位词
java·javascript·leetcode
wjcurry39 分钟前
完全和零一背包
数据结构·算法·leetcode
小彭努力中41 分钟前
147.在 Vue3 中使用 OpenLayers 地图上 ECharts 模拟飞机循环飞行
前端·javascript·vue.js·ecmascript·echarts
hie9889443 分钟前
采用最小二乘支持向量机(LSSVM)模型预测气象
算法·机器学习·支持向量机
老马聊技术44 分钟前
日历插件-FullCalendar的详细使用
前端·javascript
zhu_zhu_xia1 小时前
cesium添加原生MVT矢量瓦片方案
javascript·arcgis·webgl·cesium
咔咔一顿操作1 小时前
Cesium实战:交互式多边形绘制与编辑功能完全指南(最终修复版)
前端·javascript·3d·vue