一、Dijkstra算法的思路
Dijkstra算法是针对单源点求最短路径的算法。
其主要思路如下:
1. 将顶点分为两部分:已经知道当前最短路径的顶点集合Q和无法到达顶点集合R。
2. 定义一个距离数组(distance)记录源点到各顶点的距离,下标表示顶点,元素值为距离。源点(start)到自身的距离为0,源点无法到达的顶点的距离就是一个大数(比如Infinity)。
3. 以距离数组中值为非Infinity且值最小(即当前距离最短)的顶点V为中转跳点,假设V跳转至顶点W的距离加上顶点V至源点的距离还小于顶点W至源点的距离,那么就可以更新顶点W至源点的距离。即下面distance[V] + matrix[V][W] < distance[W],那么distance[W] = distance[V] + matrix[V][W]。
4. 重复上一步骤,即遍历距离数组,同时无法到达顶点集合R为空。
二、具体例子
偷个懒,直接用上一篇博客《最小生成树算法——Prim算法和Kruskal算法的JS实现》的图为例子。
它的邻接矩阵如下:
求解步骤
第一步:假设源点为V0,那么目前最短路径的顶点集合Q中就只有{V0}和无法到达顶点集合R中有{V1, V2, V3, V4}
第二步:初始化distance数组,就是下面这样
第三步:以distance数组中值为非Infinity且值最小的顶点为中转跳点,这一步就是V0,依照如果distance[V] + matrix[V][W] < distance[W],那么distance[W] = distance[V] + matrix[V][W]的规则,distance数组就会变成下面这样,同时集合Q变成了{V0, V1, V2, V4},集合R变成了{V3},即已访问数组为[true, true, true, false, true]。同时计算出当前距离源点最短的节点,即V2。
第四步:重复第三步的方法,然后变成以V2为中转跳点,更新距离数组,同时更新已访问数组,变成[true, true, true, true, true]。
之后同理,遍历所有节点之后,输出
三、代码实现
这个代码没有考虑权值为负数的情况,还没验证负数的情况,目前是按照权值为正数实现的,之后考虑完善。
同时这是针对单源点求最短路径,如果求全图各顶点的最短路径,只需要遍历顶点然后使用Dijkstra算法,这样算上Dijkstra算法本身的时间复杂度,总的复杂度会是O(n^3)。
/**
* Dijkstra算法:单源最短路径
* 思路:
* 1. 将顶点分为两部分:已经知道当前最短路径的顶点集合Q和无法到达顶点集合R。
* 2. 定义一个距离数组(distance)记录源点到各顶点的距离,下标表示顶点,元素值为距离。源点(start)到自身的距离为0,源点无法到达的顶点的距离就是一个大数(比如Infinity)。
* 3. 以距离数组中值为非Infinity的顶点V为中转跳点,假设V跳转至顶点W的距离加上顶点V至源点的距离还小于顶点W至源点的距离,那么就可以更新顶点W至源点的距离。即下面distance[V] + matrix[V][W] < distance[W],那么distance[W] = distance[V] + matrix[V][W]。
* 4. 重复上一步骤,即遍历距离数组,同时无法到达顶点集合R为空。
*
* @param matrix 邻接矩阵,表示图
* @param start 起点
*
*
*
* 如果求全图各顶点作为源点的全部最短路径,则遍历使用Dijkstra算法即可,不过时间复杂度就变成O(n^3)了
* */
function Dijkstra(matrix, start = 0) {
const rows = matrix.length,//rows和cols一样,其实就是顶点个数
cols = matrix[0].length;
if(rows !== cols || start >= rows) return new Error("邻接矩阵错误或者源点错误");
//初始化distance
let distance = new Array(rows).fill(Infinity);
// 初始化访问节点
let visited = new Array(rows).fill(false);
distance[start] = 0;
for(let i = 0; i < rows; i++) {
// 更新节点访问
visited[start] = true
// 达到不了的顶点不能作为中转跳点
if(distance[start] < Infinity) {
for(let j = 0; j < cols; j++) {
//通过比较distance[start] + matrix[start][j]和distance[j]的大小来决定是否更新distance[j]。
if(matrix[start][j] + distance[start] < distance[j]) {
distance[j] = matrix[start][j] + distance[start];
}
}
}
// 找到当前最短路径顶点作为中转跳点
let minIndex = -1;
let min = Infinity;
for(let k = 0; k < rows; k++) {
if (!visited[k] && distance[k] < min) {
min = distance[k];
minIndex = k;
}
}
start = minIndex
}
return distance;
}
/**
* 邻接矩阵
* 值为顶点与顶点之间边的权值,0表示无自环,一个大数表示无边(比如10000)
* */
const MAX_INTEGER = Infinity;//没有边或者有向图中无法到达
const MIN_INTEGER = 0;//没有自环
const matrix= [
[MIN_INTEGER, 9, 2, MAX_INTEGER, 6],
[9, MIN_INTEGER, 3, MAX_INTEGER, MAX_INTEGER],
[2, 3, MIN_INTEGER, 5, MAX_INTEGER],
[MAX_INTEGER, MAX_INTEGER, 5, MIN_INTEGER, 1],
[6, MAX_INTEGER, MAX_INTEGER, 1, MIN_INTEGER]
];
console.log(Dijkstra(matrix, 0));//[ 0, 5, 2, 7, 6 ]
四、优化
从上面的解析步骤中可以发现,当前如果所有节点都已经访问过,其实就已经拿到最优解,此时可以结束代码,直接输出。所以最终可以优化代码为如下:
function Dijkstra(matrix, start = 0) {
const rows = matrix.length,//rows和cols一样,其实就是顶点个数
cols = matrix[0].length;
if(rows !== cols || start >= rows) return new Error("邻接矩阵错误或者源点错误");
//初始化distance
let distance = new Array(rows).fill(Infinity);
// 初始化访问节点
let visited = new Array(rows).fill(false);
distance[start] = 0;
// 存在节点未访问则循环
while(visited.some(item => !item)) {
// 更新节点访问
visited[start] = true
// 达到不了的顶点不能作为中转跳点
if(distance[start] < Infinity) {
for(let j = 0; j < cols; j++) {
//通过比较distance[start] + matrix[start][j]和distance[j]的大小来决定是否更新distance[j]。
if(matrix[start][j] + distance[start] < distance[j]) {
distance[j] = matrix[start][j] + distance[start];
}
}
}
// 找到当前最短路径顶点作为中转跳点
let minIndex = -1;
let min = Infinity;
for(let k = 0; k < rows; k++) {
if (!visited[k] && distance[k] < min) {
min = distance[k];
minIndex = k;
}
}
start = minIndex
}
return distance;
}
五、带路径节点
改变distance数组的元素,不在只记录距离,同时记录“中转节点”。
function Node(val, pre) {
this.val = val // 当前距离
this.pre = pre || null // 中转(前置)节点
}
function Dijkstra(matrix, start = 0) {
const rows = matrix.length,//rows和cols一样,其实就是顶点个数
cols = matrix[0].length;
if(rows !== cols || start >= rows) return new Error("邻接矩阵错误或者源点错误");
//初始化distance
let distance = new Array(rows)
for (let i =0;i<rows;++i) {
distance[i] = new Node(Infinity)
}
// 初始化访问节点
let visited = new Array(rows).fill(false);
distance[start] = new Node(0);
// 存在节点未访问则循环
while(visited.some(item => !item)) {
// 更新节点访问
visited[start] = true
// 达到不了的顶点不能作为中转跳点
if(distance[start].val < Infinity) {
for(let j = 0; j < cols; j++) {
//通过比较distance[start] + matrix[start][j]和distance[j]的大小来决定是否更新distance[j]。
if(matrix[start][j] + distance[start].val < distance[j].val) {
distance[j].val = matrix[start][j] + distance[start].val;
distance[j].pre = start
}
}
}
// 找到当前最短路径顶点作为中转跳点
let minIndex = -1;
let min = Infinity;
for(let k = 0; k < rows; k++) {
if (!visited[k] && distance[k].val < min) {
min = distance[k].val;
minIndex = k;
}
}
start = minIndex
}
return distance;
}