这篇文章分享 Dijkstra 算法的 JS 代码实现。超级简单清晰,快来看看吧
Dijkstra算法是一种用于寻找加权图中最短路径的算法。它从给定节点出发,逐步扩展其邻接节点,并更新最短路径。Dijkstra算法的核心思想是贪心算法,每次选择具有最短路径的邻接节点进行扩展
Dijkstra 算法和 prim 算法过程相同,都是会从下一个最小的边开始。不同的是 prim 算法会生成最小生成树,而 dijsktral 会得到从起始节点到各个节点的最短路径
下面开始
准备数据
这个图来自王道考研的数据结构
视频课程中的截图。B 站可搜王道考研数据结构
首先准备一个有向图的邻接表数据,邻接表是按照上面图片生成的
javascript
const nodeCount = 5; // 定义节点数量
/**@type {number[]} */
let graphNodes = Array(nodeCount) // 创建一个具有nodeCount个元素的数组,并用0填充
.fill(0)
.map((item, index) => index); // 将数组中的每个元素替换为它们的索引值
/**@type {number[][]} */
const graphEdges = { // 定义表示图的边的对象
0: [ // 节点0的邻接节点和权重
[1, 10],
[4, 5],
],
1: [ // 节点1的邻接节点和权重
[2, 1],
[4, 2],
],
2: [[3, 4]], // 节点2的邻接节点和权重
3: [ // 节点3的邻接节点和权重
[0, 7],
[2, 6],
],
4: [ // 节点4的邻接节点和权重
[1, 3],
[2, 9],
[3, 2],
],
};
/**
*
* @param {number[]} graphNodes
* @param {number[][]} graphEdges
* @returns {nodeType[]}
*/
const generateGraph = (graphNodes, graphEdges) => {
// 将graphNodes数组中的每个元素转换为具有value和next属性的对象
graphNodes = graphNodes.map((item, index) => {
const tempNode = { value: index, next: null };
return tempNode;
});
// 遍历graphEdges对象,并为每个节点设置next属性
Object.entries(graphEdges).map(([value, edges]) => {
graphNodes[value].next = edges.reduce((res, [toNode, power]) => {
// 为每个邻接节点创建一个新的对象,并将其添加到结果对象的next属性中
let tempRes = res;
while (tempRes.next) tempRes = tempRes.next;
tempRes.next = { value: toNode, power, next: null };
return res;
}, {}).next;
});
// 返回生成的图的节点数组
return graphNodes;
};
const graph = generateGraph(graphNodes, graphEdges);
首先,代码定义了一个变量nodeCount,表示图中的节点数量。
然后,使用Array.from()方法创建一个具有nodeCount个元素的数组,并用0填充。接着,使用map()方法快速生成了一个节点的数组。表示节点是从 0 到 4
接下来,定义了一个表示图的边的对象graphEdges。这个对象将每个节点的邻接节点和权重存储在一个数组中。例如,graphEdges[0]表示节点0的邻接节点和权重,graphEdges[1]表示节点1的邻接节点和权重。
然后定义了一个用于生成图的函数generateGraph。该函数接受两个参数:graphNodes和graphEdges。graphNodes是一个表示节点值的数组,graphEdges是一个表示图的边的数组。函数的返回值是一个邻接表
generateGraph函数,首先遍历graphNodes数组,并为每个节点创建一个新的对象,其中包含value(节点值)和next(下一个节点)属性。然后,使用Object.entries()方法遍历graphEdges对象,并为每个节点设置next属性。最后,generateGraph函数返回生成的图的节点数组。
dijkstra
Dijkstra算法的步骤如下:
- 选择一个起点,将其标记为已访问,并将其距离设置为0。
- 遍历起点邻接的节点,计算与起点的距离,并更新最短路径。
- 重复步骤2,直到所有节点都被访问过为止。
- 返回终点的最短路径。
下面是代码实现
javascript
const isVisited = Array(nodeCount).fill(false); // 定义一个记录节点是否已被访问过的数组
const lowCast = Array(nodeCount).fill(-1); // 定义一个记录节点到起点的最短路径的数组
const path = Array(nodeCount).fill(-1); // 定义一个记录路径上的前一个节点的数组
const startNode = 0; // 选择一个起点
isVisited[startNode] = true; // 将起点标记为已访问
lowCast[startNode] = 0; // 将起点到起点的距离设置为0
path[startNode] = -1; // 将起点路径的前一个节点设置为-1
const dijkstra = (graph, currentNode) => { // 定义一个Dijkstra算法函数
// 更新路径
for (let node = graph[currentNode].next; node !== null; node = node.next) { // 遍历当前节点的邻接节点
if (!isVisited[node.value]) { // 如果节点未被访问过
if (lowCast[node.value] == -1 || lowCast[node.value] > node.power + lowCast[currentNode]) { // 如果节点到起点的最短路径比当前记录的路径长
lowCast[node.value] = node.power + lowCast[currentNode]; // 更新节点到起点的最短路径
path[node.value] = currentNode; // 更新路径上的前一个节点
}
}
}
// 找到下一个未访问过的节点
let minPower = Infinity; // 初始化最小路径
let minIndex = -1; // 初始化最小节点的索引
for (let i = 0; i < isVisited.length; i++) { // 遍历所有节点
if (isVisited[i] == false && lowCast[i] < minPower && lowCast[i] !== -1) { // 如果节点未被访问过且路径比当前记录的路径短
minPower = lowCast[i]; // 更新最小路径
minIndex = i; // 更新最小节点的索引
}
}
if (minIndex == -1) return; // 如果所有节点都已访问过,返回
// 将找到的节点标记为已访问
isVisited[minIndex] = -1; // 将找到的节点标记为已访问
dijkstra(graph, minIndex); // 递归调用Dijkstra算法,以找到该节点的所有邻接节点
};
代码比较简单,下面简单描述一下过程:
首先,代码定义了三个数组:isVisited
用于记录节点是否已被访问过,lowCast
用于记录节点到起点的最短路径,path
用于记录路径上的前一个节点。
接着,代码选择一个起点,将其标记为已访问,并将其距离设置为0。
然后,定义了一个名为dijkstra的函数,该函数接受两个参数:graph表示图的邻接表,currentNode表示当前节点。
在dijkstra函数中,首先更新路径。然后,找到下一个未访问过的节点,并检查其到起点的距离是否比当前记录的最短路径短。如果是,则更新最短路径和路径上的前一个节点。接着,找到一个未访问过的节点,并将其标记为已访问。然后,递归调用dijkstra函数,以找到该节点的所有邻接节点。
执行代码
javascript
dijkstra(graph, startNode);
console.log(lowCast, path);
//[ 0, 8, 9, 7, 5 ] [ -1, 4, 1, 4, 0 ]
lowCast数组记录了每个节点到起点的最短路径,所以lowCast数组的每个元素都表示一个节点到起点的最短路径长度。而 path数组记录了每个节点的前一个节点,所以path数组的每个元素都表示一个节点的前一个节点。
在上述例子中,lowCast数组中的元素分别为0、8、9、7、5,分别表示节点0到节点A、B、C、D、E的最短路径长度。
path数组中的元素分别为-1、4、1、4、0,分别表示节点A、B、C、D、E的前一个节点。其中,-1表示没有前一个节点,即节点0的前一个节点是-1。节点 1 的前一个及诶单是 4 等等
通过这两个数组,我们可以找到从节点0到节点4的最短路径,即:从节点0到节点4的最短路径长度为8。节点 0 到节点 4 的路径是 0->4
先在 path 数组中找到第 4 个元素,他的值表示前驱节点,path[4] 刚好是 0, 所以节点 0 到节点 4 的路径是 0->4
下面再试试找节点 0 到节点 3 的路径 。先在先在 path 数组中找到第 3 个元素,path[3] 的值是 4,表示 3 的前驱节点是 4 。然后找节点 4 的前驱,一直往前找,直到找到节点 0 。我们已经知道节点 4 的前驱是 0,就直接得到节点 0 到节点 3 的路径是 0->3->4
.
这两个数组就是这样用的。但有些麻烦,可以用代码打印出最短的路径
打印最短路径
javascript
const findPath = (node) => {
// 定义一个变量castValue,表示从节点node到起点的最短路径长度
const castValue = lowCast[node];
// 定义一个空数组castPath,用于存储从节点node到起点的最短路径
const castPath = [];
// 使用while循环,当node不等于-1时,执行以下操作:
while (node !== -1) {
// 将node添加到castPath数组的开头
castPath.unshift(node);
// 将node更新为path数组中node所指向的节点
node = path[node];
}
// 输出从节点node到起点的最短路径长度castValue
console.log("the path cast is: ", castValue);
// 输出从节点node到起点的最短路径castPath
console.log("the path is : ", castPath);
};
执行代码
javascript
findPath(2);
// the path cast is: 9
// the path is : [ 0, 4, 1, 2 ]
findPath(3);
// the path cast is: 7
// the path is : [ 0, 4, 3 ]
findPath(4);
// the path cast is: 5
// the path is : [ 0, 4 ]
有了 findPath 函数,是不是方便多啦😄
总结
这篇文章分享了 JS 代码实现 dijkstra 算法。代码清晰,注释详细,是一篇不可多的的好文章啊
之前在大学学的时候,不明觉厉,现在用代码实现一遍,发现还是很简单的,而且代码实现能够使理解更深刻。
纸上得来终觉浅,觉知此事要 coding。下篇文章分享floyd算法,喜欢可以关注一下呀