Dijkstra算法(迪杰斯特拉算法)用于在给定图中查找某个顶点到其它顶点的最短路径。
要想彻底理解 Dijkstra 算法,读者首先要搞清楚什么是最短路径。
最短路径的含义
对于逻辑关系为“多对多”的数据集,数据结构中推荐用图结构来存储。
在给定的一张图中,一个顶点到另一个顶点的路径可能有多条,最短路径指的就是顶点之间“最短”的路径。
举个简单的例子:
上图不仅是一张图,顶点之间的边还带有权值,比如 V1 到 V2 这条边的权值为 5,因此更确切的说,这是一张网,而且是有向网。
在不同的场景中,路径“最短”的含义也有所差异,比如途径顶点数量最少、总权值最小等。提到最短路径,往往指的是总权值最小的路径,所以常常在网结构(带权的图)中讨论最短路径问题,包括有向网和无向网。
关于图结构的定义和分类,不清楚的读者可以看一篇文章:图存储结构,数据结构中的图从本节开始,我将为大家介绍最后一种存储结构,也是数据结构中最复杂、难掌握的存储结构 图 。 图结构常用来存储逻辑关系为多对多的数据。比如说,一个学生可以同时选择多门课
https://xiexuewu.github.io/view/338.html
观察上图中的这张网,从 V0 到 V5 的路径有多条,包括:
- V0 -> V5,总权值为 100
- V0 -> V4 -> V5,总权值为 30+60 = 90
- V0 -> V4 -> V3 -> V5,总权值为 30+20+10 = 60
- V0 -> V2 -> V3 -> V5,总权值为 10+50+10 = 70
通过比较这些路径的总权值,发现第 3 条路径的权值最短,因为 V0 -> V4 -> V3 -> V5 就是从 V0 到 V5 的最短路径。
现如今,大家出行再也不用担心找不到路了,车上有车载导航,手机上也可以安装各种导航 App,只要输入目的地,导航会自动帮我们规划一条距离最短的路线,这是最短路径在实际生活中的典型应用之一。
在指定的一张图或者网中查找最短路径,常用的解决方案有两个,分别是 Dijkstra 算法和 Floyd(弗洛伊德)算法。本节我重点讲解 Dijkstra 算法,Floyd 算法会单独写一篇文章介绍。
Dijkstra算法
在给定的带权图(网)中,Dijkstra算法(迪杰斯特拉算法)可以找到从某个顶点到其它所有顶点的最短路径。
Dijkstra算法既适用于有向带权图,也适用于无向带权图。
需要注意的是,用Dijkstra算法查找最短路径时,必须保证图中所有边(弧)的权值为非负数,否则会导致查找失败。
首先通过一个实例,给大家展示 Dijkstra 算法查找最短路径的过程。
图 1 有向带权图
在图 1 中查找 V0 到其它顶点的最短路径,Dijkstra算法的整个查找过程是:
1) 统计从 V0 到其它顶点的权值,如下表所示:
表 2 V0->V2 是最短路径
∞
表示 V0 到目标顶点之间不存在弧,它们之间的距离为无穷大。
表格中 V0->V2 的权值最小,可以断定 V0 到 V2 的最短路径就是 V0->V2。
断定 V0-V2 是最短路径的依据是:
假设存在一条比 V0->V2 权值更小的路径,比如用 V0->Vj -> ... ->V2 表示,那么 V0->Vj 的权值一定比 V0->V2 小,表格中权值最小的路径就应该选择 V0->Vj 而不是 V0->V2,但显然这种假设是不成立的。
后续断定某个路径为最短路径时,也可以采用此方法验证。
2) 找到 V0->V2 最短路径后,继续从 V2 出发,统计从 V0 经过 V2 再到达其它顶点的路径权值,如果比表 2 记录的权值更小,表明找到了一条更短的路径,将此路径信息更新到表格中。
表 3 找到了一条从 V0 到 V3 更短的路径
从 V0->V2 继续出发,可以找到 V0->V2->V3 路径的权值比先前记录的 V0->V3 更小,因此将 V0->V2->V3 这条路径的信息更新到表中。
除了已知的最短路径外,表 3 中 V0->V4 的权值最小,所以 V0 到 V4 的最短路径就是 V0->V4。
表 4 V0->V4 是最短路径
3) 找到 V0->V4 最短路径后,继续从 V4 出发,统计从 V0 经过 V4 再到达其它顶点的路径权值,如果比表 4 记录的权值更小,表明找到了一条更短的路径,将此路径信息更新到表格中。
表 5 找到了两条更短的路径
从 V0->V4 出发,可以找到一条比 V0->V5 更短的路径 V0->V4->V5,还可以找到一条比 V0->V2->V3 更短的路径 V0->V4->V5。
除了已知的最短路径之外,表 5 中 V0->V4->V3 的权值最小,所以 V0 到 V3 的最短路径就是 V0->V4->V3。
表 6 V0->V4->V3 是最短路径
4) 找到 V0->V4->V3 最短路径后,继续从 V3 出发,统计从 V0 经过 V4 和 V3 再到达其它顶点的权值,如果比表 6 记录的权值更小,表明找到了一条更短的路径,将此路径信息更新到表格中。
从 V0->V4->V3 出发,可以找到一条比 V0->V4->V5 更短的路径 V0->V4->V3->V5,将此路径信息更新到表格中。
表 7 找到了一条从 V0 到 V5 更短的路径
除了已知的最短路径外,表 7 中 V0->V4->V3->V5 的权值最小,因此断定 V0 到 V5 的最短路径就是 V0->V4->V3->V5。
表 8 V0->V4->V3->V5 是最短路径
5) 从图 1 中可以看到,V5 顶点的出度为 0,因此无法从 V5 出发找到前往其它顶点的路径,整个算法执行结束。表 8 中记录的路径就是从 V0 到各个顶点的最短路径,其中 V0 无法达到 V1,因此路径的权值用 ∞
来表示。
Dijkstra算法的具体实现
实现 Dijkstra 算法,可以参考如下的 C 语言程序:
#include <stdio.h>
#define MAX_VERtEX_NUM 20 //顶点的最大个数
#define VRType int //表示弧的权值的类型
#define VertexType int //图中顶点的数据类型
#define INFINITY 65535
typedef struct {
VertexType vexs[MAX_VERtEX_NUM]; //存储图中顶点数据
VRType arcs[MAX_VERtEX_NUM][MAX_VERtEX_NUM]; //二维数组,记录顶点之间的关系
int vexnum, arcnum; //记录图的顶点数和弧(边)数
}MGraph;
typedef int PathMatrix[MAX_VERtEX_NUM]; //此类型数组用来存储最短路径中经过的顶点的下标
typedef int ShortPathTable[MAX_VERtEX_NUM]; //此类型数组用来存储各个最短路径的权值和
//根据顶点本身数据,判断出顶点在二维数组中的位置
int LocateVex(MGraph* G, VertexType v) {
int i = 0;
//遍历一维数组,找到变量v
for (; i < G->vexnum; i++) {
if (G->vexs[i] == v) {
break;
}
}
//如果找不到,输出提示语句,返回-1
if (i > G->vexnum) {
printf("no such vertex.\n");
return -1;
}
return i;
}
//构造有向网
void CreateUDG(MGraph* G) {
int i, j, v1, v2, w, n, m;
scanf("%d,%d", &(G->vexnum), &(G->arcnum));
for (i = 0; i < G->vexnum; i++) {
scanf("%d", &(G->vexs[i]));
}
for (i = 0; i < G->vexnum; i++) {
for (j = 0; j < G->vexnum; j++) {
G->arcs[i][j] = INFINITY;
}
}
for (i = 0; i < G->arcnum; i++) {
scanf("%d,%d,%d", &v1, &v2, &w);
n = LocateVex(G, v1);
m = LocateVex(G, v2);
if (m == -1 || n == -1) {
printf("no this vertex\n");
return;
}
G->arcs[n][m] = w;
}
}
//迪杰斯特拉算法,v0表示有向网中起始点所在数组中的下标
void ShortestPath_Dijkstra(MGraph G, int v0, PathMatrix P, ShortPathTable D) {
int final[MAX_VERtEX_NUM] = { 0 };//用于存储各顶点是否已经确定最短路径的数组
int k = 0, v, i, w;
//对各数组进行初始化
for (v = 0; v < G.vexnum; v++) {
D[v] = G.arcs[v0][v];
}
//由于下标v0的顶点为起始点,所以不用再判断
D[v0] = 0;
final[v0] = 1;
for (i = 0; i < G.vexnum; i++) {
int min = INFINITY;
//选择到各顶点权值最小的顶点,即为本次能确定最短路径的顶点
for (w = 0; w < G.vexnum; w++) {
if (!final[w]) {
if (D[w] < min) {
k = w;
min = D[w];
}
}
}
//设置该顶点的标志位为1,避免下次重复判断
final[k] = 1;
//对v0到各顶点的权值进行更新
for (w = 0; w < G.vexnum; w++) {
if (!final[w] && (min + G.arcs[k][w] < D[w])) {
D[w] = min + G.arcs[k][w];
P[w] = k;//如果<k,w>是组成最短路径的部分,只需要将 k 存储在 P 数组相应位置即可
}
}
}
}
//递归输出顶点之间最短路径的具体线路
void printPath(PathMatrix P, int i)
{
int k = P[i];
if (k == 0)
return;
printPath(P,k);
printf("V%d-", k);
}
// 输出 v0 和各个顶点之间的最短路径
void printMatrix(MGraph G, int v0, PathMatrix P, ShortPathTable D) {
int i, j;
for (i = 0; i < G.vexnum; i++) {
if (i == v0) {
continue;
}
printf("V%d-V%d: 最短路径为:", v0, i);
if (D[i]== INFINITY)
printf("%s\n", "INF");
else {
printf("%d", D[i]);
printf(",依次经过:V%d-", v0);
//调用递归函数
printPath(P, i);
printf("V%d\n", i);
}
}
}
int main() {
PathMatrix P = { 0 };
ShortPathTable D = { 0 };
MGraph G;
CreateUDG(&G);
ShortestPath_Dijkstra(G, 0, P, D);
printMatrix(G, 0, P, D);
return 0;
}
程序中,存储有向带权图采用的是顺序存储结构。
存储某个顶点到其它顶点的最短路径,用一维数组就可以实现。以表 8 中 V0->V4->V3->V5 这条从 V0 到 V5 的最短路径为例,它其实就是 V0->V4->V3 和 V3->V5 的组合,而前者是 V0->V3 的最短路径,可以看做是 V0->V4 和 V4->V3 的组合。程序中,存储最短路径的一维数组(名称为 P)为:
图 9 存储最短路径的一维数组
下标 0~5 对应的分别是 V0~V5,根据数组中存储的数据,查找 V0->V5 最短路径的过程是:
- 下标 5 的位置存储的数字是 3,表明 V3->V5 是 V0->V5 最短路径的组成部分,继续查找 V0->V3 的最短路径;
- 下标 3 的位置存储的数字是 4,表明 V4->V3 是 V0->V5 最短路径的组成部分,继续查找 V0->V4 的最短路径;
- 下标 4 的位置存储的数组是 0,表明 V0->V4 是 V0->V5 最短路径的组成部分。
因此 V0->V5 的最短路径就是 V0->V4->V3->V5。
将图 1 中的有向网输入到程序中,运行结果为:
6,8
0
1
2
3
4
5
0,5,100
0,4,30
0,2,10
1,2,5
2,3,50
3,5,10
4,3,20
4,5,60
V0-V1: 最短路径为:INF
V0-V2: 最短路径为:10,依次经过:V0-V2
V0-V3: 最短路径为:50,依次经过:V0-V4-V3
V0-V4: 最短路径为:30,依次经过:V0-V4
V0-V5: 最短路径为:60,依次经过:V0-V4-V3-V5
总结
Dijkstra算法(迪杰斯特拉算法)可以在给定网(带权图)中查找一个顶点到其它顶点的最短路径,算法的时间复杂度为 O(n^2)
。
如果需要查找网中任意两个顶点之间的最短路径,虽然Dijkstra算法也能解决(对每个顶点都执行一次Dijkstra算法),但更建议使用:Floyd弗洛伊德算法。