【数据结构】——图的最短路径算法(迪杰斯特拉+弗洛伊德)

两种算法的动态演示:视频地址

另两种图的最短路径算法,贝尔曼-福特&SPFA:https://blog.csdn.net/namewdy/article/details/106330691

下面代码都以此图为例,求顶点A与顶点E的最短路径长度及最短路径。(最短路径:A B G F C I E,最短路径长度24)
【数据结构】——图的最短路径算法(迪杰斯特拉+弗洛伊德)_第1张图片

1.迪杰斯特拉(Dijkstra)

迪杰斯特拉,也有人叫作狄克斯特拉,该算法是贪心思想的运用。具体表现在:如果源点A到终点B的距离最短,那么这条路径上的其它顶点到源点A的距离也是最短的。这点很容易理解,如果其它顶点到源点有更短的路径,那么A到B必然存在一条更小的最短路径。

迪杰斯特拉算法的求解步骤是:从源点出发,首先将离它距离最近的那个顶点加入到最短路径当中。然后以这个顶点为中间顶点对所有非最短路径上的顶点做一次松弛操作得到所有非最短路径上的顶点到源点的当前最短距离,把最小距离的那个顶点加入到最短路径当中。重复以刚加入的这个顶点作为中间顶点对剩下的所有非最短路径上的顶点做松弛操作,然后又找到一个最小距离的顶点作为最短路径上的顶点。这样每一轮松弛操作就能得到一个最短路径上的顶点,直到找全最短路径。

算法过程可以概括为深度搜索与贪心思想的结合。深度搜索体现在每确定一个最短路径上的顶点时,以该顶点为中间顶点对所有非最短路径上的顶点做一次松弛操作,确定所有非最短路径上的顶点到源点的当前最短距离。贪心体现在每次都是从这些当前最短距离中选距离最小的顶点加入到最短路径中。整个求解过程可以看作是逐步增加最短路径上的顶点的过程。

什么是松弛操作:对一条边(u, v)松弛操作,其实就是测试是否可以通过 u,对迄今找到的 v 的最短路径进行改进。例如用 dist[u], dist[v] 表示顶点 u, v 到源点的最短距离,且当前已求出 dist[u]=3, dist[v]=8,而顶点 u, v 之间有一条权值为2的边edge[u][v],由于 dist[u]+edge[u][v]

Dijkstra算法在具体实现时,需要设置3个辅助数组:

  • set数组:用来标识某个顶点到源点的最短距离是否已确定,set[ i ]=true表示编号为 i 的顶点到源点的最短距离已确定。
  • dist数组:用来保存目前已求出的各个顶点到源点的最短距离,例如dist[ i ]存储编号为 i 的顶点到源点的目前最短距离。
  • path数组:用来保存最短路径。path[ i ]保存的是源点与编号为 i 的顶点的最短路径中顶点 i 的前一个顶点。这样便可以反推出任意一个顶点到源点最短路径上的所有顶点。

java代码:

public class Dijkstra {
	char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I' };
	int[][] matrix = new int[9][9];
	int INF = 1 << 31 - 1; 							// INF表示正无穷

	// 创建邻接矩阵
	private void creatMartix() {
		matrix[locate('A')][locate('B')] = matrix[locate('B')][locate('A')] = 4;
		matrix[locate('A')][locate('G')] = matrix[locate('G')][locate('A')] = 8;
		matrix[locate('B')][locate('G')] = matrix[locate('G')][locate('B')] = 3;
		matrix[locate('B')][locate('C')] = matrix[locate('C')][locate('B')] = 8;
		matrix[locate('G')][locate('F')] = matrix[locate('F')][locate('G')] = 1;
		matrix[locate('G')][locate('H')] = matrix[locate('H')][locate('G')] = 6;
		matrix[locate('C')][locate('F')] = matrix[locate('F')][locate('C')] = 2;
		matrix[locate('F')][locate('H')] = matrix[locate('H')][locate('F')] = 6;
		matrix[locate('C')][locate('D')] = matrix[locate('D')][locate('C')] = 7;
		matrix[locate('C')][locate('I')] = matrix[locate('I')][locate('C')] = 4;
		matrix[locate('H')][locate('I')] = matrix[locate('I')][locate('H')] = 2;
		matrix[locate('D')][locate('I')] = matrix[locate('I')][locate('D')] = 14;
		matrix[locate('D')][locate('E')] = matrix[locate('E')][locate('D')] = 9;
		matrix[locate('E')][locate('I')] = matrix[locate('I')][locate('E')] = 10;
		
		for (int i = 0; i < matrix.length; i++) {
			for (int j = 0; j < matrix.length; j++) {
				if (matrix[i][j] == 0) {
					matrix[i][j] = INF;
				}
			}
		}
	}

	private int locate(char v) {
		int i = 0;
		for (; i < vertex.length; i++) {
			if (v == vertex[i])
				break;
		}
		return i;
	}

	/*
	 * --------------------上面是创建邻接矩阵,以下是Dijkstra部分代码--------------------
	 */

	// dist[i]保存源点到顶点i的当前最短路径长度,初值为边上的权值
	int[] dist = new int[vertex.length];
	// path[j]保存源点与顶点j的当前最短路径中顶点j前驱顶点的编号,初值为源点start的编号或-1
	int[] path = new int[vertex.length];
	// 保存已确定的到源点距离最短的顶点集合
	boolean[] set = new boolean[vertex.length];

	private void dijkstra(char start, char end) {
		// 初始化dist数组和set数组初值
		for (int i = 0; i < vertex.length; i++) {
			dist[i] = matrix[locate(start)][i];
			set[i] = false;
			// 到邻接点的最短路径前驱顶点设为源点,其余设为-1
			if (matrix[locate(start)][i] < INF) {
				path[i] = locate(start);
			} else {
				path[i] = -1;
			}
		}

		// 源点最短距离设为0
		dist[0] = 0;
		// 源点加入set集合中
		set[locate(start)] = true;

		int u = 0;
		int mindis;
		// 遍历vertex.length-1次;每次找出最短路径中的一个顶点
		for (int i = 1; i < vertex.length; i++) {
			mindis = INF;
			// 从所有未确定最短距离的顶点中找出距源点最短的那个顶点
			for (int j = 0; j < vertex.length; j++) {
				if (set[j] == false && dist[j] < mindis) {
					u = j;
					mindis = dist[j];
				}
			}

			set[u] = true; 							// 将新确定最短路径的顶点加入到set集合中

			// 以刚确定的最短距离顶点作为中间顶点,更新所有剩余的未确定最短距离的顶点到源点的最短距离。
			// 具体是:如果源点经过该中间顶点到顶点j的距离小于原有到顶点j距离时,更新到j最短距离,
			// 并将中间顶点作为到顶点j最短路径中顶点j的前驱顶点。否则不变。
			for (int j = 0; j < vertex.length; j++) {
				if (set[j] == false) {
					if (matrix[u][j] < INF && dist[u] + matrix[u][j] < dist[j]) {
						dist[j] = dist[u] + matrix[u][j];
						path[j] = u;
					}
				}
			}
		}
		System.out.println("最短路径长度:" + dist[locate(end)]);
		getPath(end);
	}

	// 根据path数组打印路径
	private void getPath(char end) {
		System.out.print("最短路径:" + end);
		int i = locate(end);
		while (path[i] != -1) {
			System.out.print("<--" + vertex[path[i]]);
			path[i] = path[path[i]];
		}
	}

	public static void main(String[] args) {
		Dijkstra d = new Dijkstra();
		d.creatMartix();
		d.dijkstra('A', 'E');
	}
}

算法分析

  • 算法中包含两重循环,所以时间复杂度 O ( n 2 ) Ο(n^2) O(n2)
  • 空间复杂度 O ( n ) Ο(n) O(n),为3个辅助数组所占的空间。
  • 适用条件:该算法不适用含有负边的图,因为Dijkstra算法中将所有顶点分为已求得最短路径的顶点集合(set)和未确定最短路径的顶点集合。归入set集合顶点的最短路径及其长度不再改变,如果边上的权值允许为负值,那么有可能出现set内某顶点(记为a)的最短路径长度加上这条负边的权值结果小于a原先确定的最短路径长度,而此时a在Dijkstra算法下是无法更新的,由此便可能得不到正确的结果。
  • Dijkstra算法属于图的单源最短路径算法,得到的是一个数组,表示源点到其它各点的最短距离。 如果只是想得到某两个顶点间的最短路径,只需要在每次新确定一个最短路径的顶点时判断这个顶点是不是终点,是的话直接break退出循环。

2.弗洛伊德(Floyd)

弗洛伊德算法采用动态规划思想,是由局部最优解推导出整体最优解的过程。用二维数组 dist 保存算法运行过程中任意两顶点间的最短路径权值,例如 dist[ i ][ j ] 的值表示编号为 i 的顶点到编号为 j 的顶点之间最短路径的权值。

dist 数组根据邻接矩阵初始化,如果顶点 i 到顶点 j 的存在边且权值为 w,则让 dist[ i ][ j ] = w,否则初始为无穷大。 然后对整个二维数组,每次通过以不同顶点作为中间顶点来更新整个 dist 数组。例如以编号为 k 的顶点作为中间顶点时,如果 dist[ i ][ j ] > dist[ i ][ k ] + dist[ k ][ j ],则说明顶点 k 更有可能是顶点 i 与顶点 j 的最短路径上的顶点。这里蕴含的思想是,如果顶点 k 是顶点 i 与顶点 j 最短路径上的顶点,那么顶点 i 到顶点 k 与顶点 k 到顶点 j 的路径同时都是最短的。

这样针对不同顶点对 dist 数组每进行一轮更新,就可以得到一个将该顶点考虑在内的最短路径权值的新 dist 数组,以此类推,直至将所有顶点考虑一遍,此时更新后得到的 dist 数组保存的就是将所有顶点考虑在内的最短路径权值。这里体现的就是上面所说的由局部最优解推导出整体最优解。

如果需要获取具体最短路径的话可以再增设一个二维数组 path[ ][ ],path[ i ][ j ] 表示顶点 i 到顶点 j 的最短路径中顶点 j 的前一个顶点的编号。初始化为如果顶点 i 到顶点 j 存在边的话用 path[ i ][ j ] 保存顶点 i 的编号,否则设为-1(起个标志作用,也可以是其它)。以后在对各个顶点考虑的时候,每次 dist[ i ][ j ] 的最短路径发生更新时都将 path[ i ][ j ] 修改为被考虑顶点的编号。最后依次往回推导得到最短路径上的所有顶点。

详见代码:

public class Floyd {
	char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I' };
	int[][] matrix = new int[9][9];
	int INF = 1 << 31 - 1;

	private void creatMartix() {
		matrix[locate('A')][locate('B')] = matrix[locate('B')][locate('A')] = 4;
		matrix[locate('A')][locate('G')] = matrix[locate('G')][locate('A')] = 8;
		matrix[locate('B')][locate('G')] = matrix[locate('G')][locate('B')] = 3;
		matrix[locate('B')][locate('C')] = matrix[locate('C')][locate('B')] = 8;
		matrix[locate('G')][locate('F')] = matrix[locate('F')][locate('G')] = 1;
		matrix[locate('G')][locate('H')] = matrix[locate('H')][locate('G')] = 6;
		matrix[locate('C')][locate('F')] = matrix[locate('F')][locate('C')] = 2;
		matrix[locate('F')][locate('H')] = matrix[locate('H')][locate('F')] = 6;
		matrix[locate('C')][locate('D')] = matrix[locate('D')][locate('C')] = 7;
		matrix[locate('C')][locate('I')] = matrix[locate('I')][locate('C')] = 4;
		matrix[locate('H')][locate('I')] = matrix[locate('I')][locate('H')] = 2;
		matrix[locate('D')][locate('I')] = matrix[locate('I')][locate('D')] = 14;
		matrix[locate('D')][locate('E')] = matrix[locate('E')][locate('D')] = 9;
		matrix[locate('E')][locate('I')] = matrix[locate('I')][locate('E')] = 10;

		for (int i = 0; i < matrix.length; i++) {
			for (int j = 0; j < matrix.length; j++) {
				if (matrix[i][j] == 0) {
					matrix[i][j] = INF;
				}
			}
		}
	}

	private int locate(char v) {
		int i = 0;
		for (; i < vertex.length; i++) {
			if (v == vertex[i])
				break;
		}
		return i;
	}

	/*
	 * --------------------上面创建邻接矩阵,以下是Floyd部分代码--------------------
	 */

	int[][] dist = new int[vertex.length][vertex.length];
	int[][] path = new int[vertex.length][vertex.length];

	private void floyd(char start, char end) {
		// 初始化
		for (int i = 0; i < vertex.length; i++) {
			for (int j = 0; j < vertex.length; j++) {
				dist[i][j] = matrix[i][j];
				if (i != j && matrix[i][j] < INF) {
					path[i][j] = i;
				} else {
					path[i][j] = -1;
				}
			}
		}

		// 将所有顶点逐个作为中间顶点考虑
		for (int k = 0; k < vertex.length; k++) {
			for (int i = 0; i < vertex.length; i++) {
				for (int j = 0; j < vertex.length; j++) {
					// 如果两顶点i,j之间通过顶点k的最短路径小于原来的最短路径,更新dist数组的值,并设顶点k为顶点j的前驱顶点
					if (i != j && dist[i][k] < INF && dist[k][j] < INF && dist[i][j] > dist[i][k] + dist[k][j]) {
						dist[i][j] = dist[i][k] + dist[k][j];
						path[i][j] = path[k][j];
					}
				}
			}
		}
		System.out.println("最短路径长度:" + dist[locate(start)][locate(end)]);
		getPath(start, end);
	}

	private void getPath(char start, char end) {
		int i = locate(start);
		int j = locate(end);
		System.out.print("最短路径:" + end);
		while (path[i][j] != -1) {
			// 获取最短路径根据的是源点不变,每次变更新的终点,直至终点与源点重合。
			System.out.print("<--" + vertex[path[i][j]]);
			path[i][j] = path[i][path[i][j]];
		}

//		// 获取最短路径的方法2,根据源点在二维数组中下标一定相等
//		while (i != j) {
//			System.out.print("<--" + vertex[path[i][j]]);
//			j = path[i][j];
//		}
	}

	public static void main(String[] args) {
		Floyd f = new Floyd();
		f.creatMartix();
		f.floyd('A', 'E');
	}
}

算法分析

  • 代码中包含三层for循环,所以时间复杂度 O ( n 3 ) Ο(n^3) O(n3)
  • 空间复杂度 O ( n 2 ) Ο(n^2) O(n2),为两个二维数组的空间。
  • 适用条件:该算法允许图含有负边,但不允许含有负回路。这是因为如果回路权值为负,那么走一次回路,路径长度一定比上一次小,而算法本身并不会为此多计算一次这个回路。所以这里的不适用不是指程序不能运行,而是指可能得不到正确结果。
  • Floyd算法属于图的多源最短路径算法,得到的是一个矩阵,表示任意两点间的最短距离。 对比Floyd和Dijkstra两种算法,如果需要求的是图中所有两顶点间的最短距离,Dijkstra算法需要调用 n 次(n是顶点个数),时间复杂度同样也是 O ( n 3 ) Ο(n^3) O(n3)。但因为Floyd算法使用动态规划,求整体最优解利用了局部最优解的结果,而Dijkstra算法每次都是从头开始求,所以如果是求所有两顶点之间的最短路径,弗洛伊德算法的实际运行时间比调用 n 次迪杰斯特拉算法的时间快很多。

你可能感兴趣的:(数据结构)