算法笔记-最短路径

本文深入探讨了Dijkstra算法在单源最短路径问题中的应用,详细解释了算法流程及其实现,包括如何使用小顶堆优化算法效率,以及算法的时间复杂度分析。此外,还讨论了算法在不同场景下的应用,如最少时间路径和最少红绿灯路径规划。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关于图这种数据结构总结的时候,总结了深度优先搜索和广度优先搜索,这两种算法主要是针对无权图的搜索算法。

今天总结针对有权图的搜索算法,最短路径。

常用的地图导航软件,规划行程的时候,会给出最短路径,最少用时,最少红绿灯等不同路径,这种的问题都可以最短路径算法解决。

最短路径

实现算法之初,先确定数据结构。针对地图规划,我们用有权图来表示。地图中的分叉路口映射为图中的顶点,路口与下一个路口之间的距离,映射为图上两个顶点之间边的权重。如果是单行道,画一条有向的边,如果是双行道,画两条有向的边。数据结构代码实现如下:

public class Graph {
	// 邻接表
	private LinkedList<Edge> adj[];
	// 顶点个数
	private int v;

	// 初始化有向有权图
	public Graph(int v) {
		this.v = v;
		this.adj = new LinkedList[v];
		for (int i = 0; i < v; ++i) {
			this.adj[i] = new LinkedList<>();
		}
	}

	// 添加一条边
	public void addEdge(int s, int t, int w) {
		this.adj[s].add(new Edge(s, t, w));
	}

	// 边
	private class Edge {
		// 边的起始顶点编号
		public int sid;
		// 边的终止顶点编号
		public int tid;
		// 权重
		public int w;

		// 初始化边
		public Edge(int sid, int tid, int w) {
			this.sid = sid;
			this.tid = tid;
			this.w = w;
		}
	}

	// 下面这个类是为了 dijkstra 中小顶堆对比条件  实现用的
	private class Vertex implements Comparable<Vertex> {
		public int id; // 顶点编号 ID
		public int dist; // 从起始顶点到这个顶点的距离

		public Vertex(int id, int dist) {
			this.id = id;
			this.dist = dist;
		}

		@Override
		public int compareTo(Vertex o) { // 按照 dist 从小到大排序
			if (o.dist > this.dist)
				return -1;
			else
				return 1;
		}
	}

}

Vertex 类主要是在小顶堆中用来做排序的依据。

单源最短路径

Dijkstra 算法。代码实现如下:

	// 从顶点 s 到顶点 t 的最短路径
	public void dijkstra(int s, int t) { 
		// 用来还原最短路径
		int[] predecessor = new int[this.v]; 
		// 记录起始顶点到这个顶点的距离
		Vertex[] vertexes = new Vertex[this.v]; 
		// 初始化 dist 为无穷大
		for (int i = 0; i < v; ++i) { 
			vertexes[i] = new Vertex(i, Integer.MAX_VALUE);
		}
		// 小顶堆
		PriorityQueue<Vertex> queue = new PriorityQueue<>(); 
		// 标记是否进入过队列
		boolean[] inQueue = new boolean[this.v]; 
		// 先把起始顶点放到队列中
		queue.add(vertexes[s]); 
		//自己到自己的距离为 0 
		vertexes[s].dist = 0;
		//标记已经进入队列
		inQueue[s] = true;
		
		while (!queue.isEmpty()) {
			// 小顶堆,取出 dist 最小的顶点
			Vertex minVertex = queue.poll(); 
			if (minVertex.id == t){
				break; // 最短路径产生了
			}
			//遍历该顶点指向其他定点的边
			for (int i = 0; i < adj[minVertex.id].size(); ++i) {
				// 取出一条 minVetex 相连的边
				Edge e = adj[minVertex.id].get(i); 
				// 取出顶点 minVertex 指向的下一个顶点 nextVertex
				Vertex nextVertex = vertexes[e.tid]; 
				// 找到一条到 nextVertex 更短的路径
				if (minVertex.dist + e.w < nextVertex.dist) {
					// 更新 dist
					nextVertex.dist = minVertex.dist + e.w; 
					 // 更新前驱顶点 nextVertex 的前驱顶点是 nextVertex
					predecessor[nextVertex.id] = minVertex.id;
					 // 如果没有在队列中
					if (inQueue[nextVertex.id] == false) {
						// 就把它放到队列中
						queue.add(nextVertex); 
						// 标记访问
						inQueue[nextVertex.id] = true;
					}
				}
			}
		}
		// 输出最短路径
		System.out.print(s);
		print(s, t, predecessor);
	}

	// 输出最短路径
	private void print(int s, int t, int[] predecessor) {
		if (s == t)
			return;
		print(s, predecessor[t], predecessor);
		System.out.print("->" + t);
	}

上述代码有详细的注释,但是过程有些复杂,下面在梳理一遍过程。

确定起始点,然后遍历起始点指出去的所有边,边的终点编号 tid 对应数组 predecessor 的下标(predecessor[ tid ]),边的权重 w 对应 predecessor[ tid ] 的值(predecessor[ tid ] = w),数组 predecessor 存储起始点到达该顶点的距离。

通过遍历边得到的顶点依次放入小顶堆中,并做访问标记,下一次小顶堆中距离起止点最小的顶点出列。

然后,对出列的顶点,继续上面的步骤,直到再次出列的顶点的编号就是我们要找的终点。

整个算法过程如上述描述,其中细节处理需要我们特别注意。更新起始点到访问到的顶点的距离,标记已访问的顶点,记录访问顶点的前驱访问顶点。

Dijkstra 算法的时间复杂度

上述代码中,复杂逻辑就是 while 循环体。

由代码我们可以看到,while 循环最多循环 V 次,V 代表顶点个数。

while 体中 for 循环次数不确定,可以标记为 E1,E2,一直到EV。把这些所有加起来,总和不会大于 E。for 循环中涉及从优先级队列中添加元素,更新元素,时间复杂度为 O(logV)。

所以整段代码的时间复杂度是 O(E*logV)。

补充

如果是最少时间路径规划:算法不变,边的权重改为路程时间的值,就可以解决问题。

如果是最少红绿灯路径规划:边的权重变成 1,有向图改为无向图,利用广度优先搜索,两点间的距离就是最少红绿灯路径规划。

分段规划最短路径:如果规划北京到上海的最短路径,就需要重新换一下策略。首先,地图缩小,构建北京到上海关键路口的有权有向图,规划出最短路径。然后,再对每一段路径,进行最短路径规划。

缩小范围规划最短路径:无需遍历图中所有顶点,可划定包含岂止顶点的小范围图,然后再规划最短路径。

总结

本文创作灵感来源于 极客时间 王争老师的《数据结构与算法之美》课程,通过课后反思以及借鉴各位学友的发言总结,现整理出自己的知识架构,以便日后温故知新,查漏补缺。

初入算法学习,必是步履蹒跚,一路磕磕绊绊跌跌撞撞。看不懂别慌,也别忙着总结,先读五遍文章先,无他,唯手熟尔~
与诸君共勉

关注本人公众号,第一时间获取最新文章发布,每日更新一篇技术文章。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值