【迪杰斯特拉算法思想】
设有两个顶点集合S和T,集合S中存放图中已找到最短路径的顶点,集合T存放图中剩余顶点。初始状态下,集合S中只包含源点V0。然后不断从集合T中选取到顶点V0路径长度最短的顶点Vu并入集合S中。集合S中每次并入一个新的顶点Vu后,都要修改顶点V0到集合T中顶点的最短路径长度值。不断重复此过程,直到集合T中的顶点全部并入集合S中为止。
【深入理解】
当集合T中的顶点Vu并入集合S中时,Vu被确定为最短路径上的顶点,此时Vu就像V0到达集合T中顶点的中转站,即从V0到集合T中顶点的路径条数随着顶点Vu从集合T并入到集合S中后会增加,而这些新出现的路径很有可能比原有的V0到集合T中的顶点的路径长度小,因此需要修改原有V0到集合T中的其他顶点的路径长度。对于此时集合T中的顶点Vk来说,V0不经过Vu到Vk的路径长度(即原有的路径长度)为a,另一种是V0经过Vu到Vk的路径长度(即新的路径长度)为b。此时存在两种情况:
第一:a <= b。此时什么都不做。
第二:a > b。则用b来代替a作为V0到Vk的路径长度。
【实现迪杰斯特拉算法的辅助数据结构】
为了实现迪杰斯特拉算法,需要用到3个辅助数组:dist[]、path[]和set[]。
dist[Vi]表示当前已找到的从V0到每个终点Vi的最短路径长度。初始状态:若从V0到Vi有边,则dist[Vi]为边上的权值,否者置dist[Vi]为无穷大。
path[Vi]中保存V0到Vi最短路径上Vi的前一个顶点。假设最短路径上的定点序列为V0,V1,V2,......,Vi-1,Vi,则path[Vi] = Vi-1。初始状态:如果V0到Vi有边,则path[Vi] = V0,否则path[Vi] = -1。
set[]数组为标记数组。set[Vi] = 0 表示顶点Vi在集合T中,即没有被并入最短路径;set[Vi] = 1 表示顶点Vi在集合S中,即已经被并入最短路径。初始状态:set[V0] = 1,其余元素全为0。
【迪杰斯特拉算法的执行过程】
① 从当前的dist[]数组中选出最小值,即为当前的最短路径,假设为dist[Vu]。将set[Vu]的值设置为1,代表将当前的顶点Vu从集合T中并入到集合S中。
② 循环扫描图中的顶点,对每个顶点进行以下检测:
假设当前结点为Vj,检测Vj是否已经被并入到集合S中,即检查set[Vj]的值是否为1。如果set[Vj]的值为1,则代表顶点Vj已经被并入到集合S中,什么操作都不做。如果set[Vj]的值为0,则代表当前的顶点j还没有被并入到集合S中。此时比较dist[Vj]和dist[Vu]+w的值的大小,其中w为边<Vu,Vj>的权值。这个比较就是要看V0旧的最短路径到达Vj和V0经过含有Vu的新的路径到达Vj哪个更短一些。如果dist[Vj]>dist[Vu]+w,则用新的路径长度更新就的最短路径长度,并把顶点Vu加入到路径中,且作为路径上Vj之前的那个顶点;否则,什么都不做。
③ 对①和②步操作执行n-1次(n为图中顶点的个数),即可得到顶点V0到其余各个顶点的最短路径。
【对迪杰斯特拉算法执行过程的不充分析】
当迪杰斯特拉算法执行完毕后,path[]数组中其实保存了一棵树,这是一棵用双亲存储结构存储的树。通过这棵树可以打印出从源点到任何一个顶点最短路径上所经过的所有顶点。树的双亲表示法只能直接输出由叶子结点到根结点路径上的结点,而不能逆向输出,因此需要借助一个栈来实现逆向输出,打印路径。
【逆向打印路径函数伪代码——参考自《数据结构高分笔记》】
public void PrintPath(int path[],int a){
int stack[maxsize];
int top = -1;
//这个循环以由叶子结点到根结点的顺序将其入栈
while(path[a] != -1){
stack[++top] = a;
a = path[a];
}
stack[++top] = a;
while(top != -1)
System.out.println(stack[--top]);
}【迪杰斯特拉算法实现伪代码——参考自《数据结构高分笔记》】
/*
* MGraph g:表示用邻接矩阵存储的图
* int v:表示求从顶点v到其余各个顶点的最短路径长度
* int dist[]:图中各个顶点的最短路径
* int path[]:图中各个顶点的前置结点
*
* 函数执行完毕后,dist数组中存放了v到其余各个顶点的最短路径长度。path数组中存放了v到其余各个顶点的最短路径
*/
public void Dijkstra(MGraph g,int v,int dist[],int path[]){
int set[maxsize]; //设置集合数组
for(int i = 0;i < g.n;++i){ //对数组进行初始化操作
dist[i] = g.edges[v][i]; //dist数组的初始状态为邻接矩阵中<v,i>的值
set[i] = 0; //set数组默认初始状态均为0,表示目前图中所有顶点均在集合T中
if(g.edges[v][i] < INF) //path数组初始状态,如果顶点v与顶点i有边相连,则path[i]=v;否则path[i]=-1;
path[i] = v;
else
path[i] = -1;
}
set[v] = 1; //此时将顶点v加入到集合S中
path[v] = -1; //将顶点v作为树的根结点
for(int i = 0;i < g.n;++i){
int min = INF; //设置最小值
int min_index = -1; //设置最小值数组下标
for(int j = 0;j < g.n;++j){ //通过这个循环每次从剩余顶点中选出一个顶点,使得dist[i]的值最小
if(set[j]==0 && dist[j] < min){
min_index = j;
min = dist[j];
}
set[min_index] = 1; //将选出的顶点并入到最短路径集合S中
}
for(int j = 0;j < g.n;++j){ //将刚并入到集合S中的顶点作为中间点,对所有通往剩余顶点的路径进行检测,并更新最短路径
if(set[j]==0 && dist[min_index]+g.edges[u][j] < dist[j]){
dist[j] = dist[min_index]+g.edges[u][j]; //更新当前顶点的最短路径长度
path[j] = min_index; //将中间点min_index作为此时顶点的前置顶点
}
}
}
}【迪杰斯特拉算法的时间复杂度分析】
算法的主要部分为一个双重循环,基本操作执行的总次数即为双重循环执行的次数n^2次,因此迪杰斯特拉算法的时间复杂度为O(n^2)。