🥳每日一练-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简易版 - 掘金

相关推荐
算法歌者9 分钟前
[算法]入门1.矩阵转置
算法
林开落L24 分钟前
前缀和算法习题篇(上)
c++·算法·leetcode
远望清一色25 分钟前
基于MATLAB边缘检测博文
开发语言·算法·matlab
tyler_download26 分钟前
手撸 chatgpt 大模型:简述 LLM 的架构,算法和训练流程
算法·chatgpt
SoraLuna1 小时前
「Mac玩转仓颉内测版7」入门篇7 - Cangjie控制结构(下)
算法·macos·动态规划·cangjie
我狠狠地刷刷刷刷刷1 小时前
中文分词模拟器
开发语言·python·算法
鸽鸽程序猿1 小时前
【算法】【优选算法】前缀和(上)
java·算法·前缀和
九圣残炎1 小时前
【从零开始的LeetCode-算法】2559. 统计范围内的元音字符串数
java·算法·leetcode
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf