一文搞懂 Dijkstra 算法:最短路径的经典之选(含 Java 代码详解)

在图论中,最短路径问题是非常常见的基本问题之一。Dijkstra 算法是解决单源最短路径问题中最经典、最常用的算法之一,适用于带权有向图边权非负的情况。

本文将结合一段完整的 Java 实现,带你从原理到代码逐步深入掌握 Dijkstra 算法。


一、Dijkstra 算法的基本原理

Dijkstra 的目标是:给定起点 $S$,找到图中从 $S$ 到所有点的最短路径长度

它的核心思想是:

贪心 + 已知最短路径的节点不会再被更新

算法步骤简述:

  1. 初始化:

    • 创建一个距离数组 dist[],记录起点到每个点的当前最短距离;

    • 初始时 dist[start] = 0,其余点为

  2. 使用优先队列(最小堆)维护未处理的节点,优先选择距离最短的;

  3. 每次从优先队列中取出当前最近的节点 u,进行如下操作:

    • 遍历 u 的所有邻接边 (u → v, w),如果 dist[v] > dist[u] + w,就更新并将 v 加入队列;

    • 标记 u 为已访问,之后不会再处理它;

  4. 重复此过程直到队列为空,dist[] 中就是从起点到所有点的最短路径。

贪心为什么对?

我们证明:

假设从起点到节点 u 的最短路径已找到(dist[u] 最小),则:

  • 若存在比当前路径更短的路径,那一定是通过其他未被锁定的节点;

  • 但根据优先队列机制,其他节点的距离都不可能更小(否则会先被选中);

  • 所以当前的 dist[u] 必定是最优的。

这就是 Dijkstra 的贪心选择是**“局部最优 = 全局最优”**的典范。


  算法流程(图解理解)

以图为例(起点为 0):

0 --(4)--> 1 --(2)--> 2
 \        ↘︎
 (1)      (5)
  ↓         ↘︎
  3 --(3)--> 4
  1. 初始:dist = [0, ∞, ∞, ∞, ∞]pq = [(0, 0)]

  2. 弹出点 0,更新点 1(0+4=4),点 3(0+1=1),pq = [(3,1), (1,4)]

  3. 弹出点 3,更新点 4(1+3=4),pq = [(1,4), (4,4)]

  4. 弹出点 1,更新点 2(4+2=6),点 4(不更新,因为 4 < 9),pq = [(4,4), (2,6)]

...以此类推

每次都是从未访问的点中,挑出距离最小的那个,从它继续扩展更新。


二、时间复杂度分析

使用最小堆优化后的 Dijkstra 算法的时间复杂度为:

O((V + E) log V)
  • V 是节点数;

  • E 是边数;

  • 每个节点最多被加入优先队列一次,每次操作代价是 log V

  • 每条边最多被检查一次。


三、Java 实现详解(基于真实案例)

下面是完整实现,并结合关键段落做详细解释:

Java 代码:

package ff;

import java.io.*;
import java.util.*;

public class Main {
	static FastRead in = new FastRead();
	static PrintWriter out = new PrintWriter(System.out);

	static class Edge {
		int to;
		int val;
		public Edge(int to, int val) {
			this.to = to;
			this.val = val;
		}
	}

	public static void main(String[] args) {
		int n = in.nextInt(); // 节点数
		int m = in.nextInt(); // 边数

		List> graph = new ArrayList<>();
		for (int i = 0; i < n; i++) {
			graph.add(new ArrayList<>()); // 初始化邻接表
		}

		for (int i = 0; i < m; i++) {
			int u = in.nextInt() - 1;
			int v = in.nextInt() - 1;
			int w = in.nextInt();
			graph.get(u).add(new Edge(v, w)); // 有向边 u->v
		}

		int[] dist = dij(n, graph, 0);
		int ans = dist[n - 1] == Integer.MAX_VALUE ? -1 : dist[n - 1];
		out.println(ans);
		out.flush();
		out.close();
	}

	public static int[] dij(int n, List> graph, int start) {
		int[] dist = new int[n];
		Arrays.fill(dist, Integer.MAX_VALUE);
		dist[start] = 0;

		PriorityQueue pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));
		pq.offer(new int[] { start, 0 });

		boolean[] visited = new boolean[n];

		while (!pq.isEmpty()) {
			int[] cur = pq.poll();
			int u = cur[0];
			int d = cur[1];

			if (visited[u]) continue;
			visited[u] = true;

			for (Edge edge : graph.get(u)) {
				int v = edge.to;
				int w = edge.val;
				if (dist[v] > d + w) {
					dist[v] = d + w;
					pq.offer(new int[] { v, dist[v] });
				}
			}
		}
		return dist;
	}
}

class FastRead {
	StringTokenizer st;
	BufferedReader br;

	public FastRead() {
		br = new BufferedReader(new InputStreamReader(System.in));
	}

	String next() {
		while (st == null || !st.hasMoreElements()) {
			try {
				st = new StringTokenizer(br.readLine());
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return st.nextToken();
	}

	int nextInt() {
		return Integer.parseInt(next());
	}
}

核心点说明:

  • 使用 优先队列 维护最短路径;

  • dist[] 存储最短路径长度;

  • visited[] 标记已经确定最短路径的节点;

  • 每次从队列中取出当前最小距离的点进行扩展。


输入输出格式

输入样例:

5 6
1 2 2
1 3 4
2 3 1
2 4 7
3 5 3
4 5 1

说明:

  • 有 5 个点,6 条边;

  • 边表示为 起点 终点 权重,从点 1 开始到点 5 的最短路径。

输出样例:

8

补充优化建议

  • ✅ 如果图是无向图,加边时双向:

graph.get(u).add(new Edge(v, w));
graph.get(v).add(new Edge(u, w));
  • ✅ 想输出路径的话,可以维护一个 pre[] 数组记录每个点的前驱,最后回溯路径。


总结

  • Dijkstra 算法适用于边权非负的有向图或无向图;

  • 通过优先队列可以将时间复杂度降为 O((V + E) log V)

  • 实现时要注意下标从 0 开始,避免越界;

  • 常用于图论入门、路径优化、交通网络等场景。


练习题推荐

  • Leetcode 743. 网络延迟时间

  • AcWing 849. Dijkstra求最短路 I

  • 洛谷 P3371 【模板】单源最短路径(弱化版)


如果这篇文章对你有帮助,欢迎 点赞 + ⭐收藏 + 评论,让更多人一起掌握图论的核心算法!有任何问题也欢迎留言交流哦~

你可能感兴趣的:(java,算法,最短路,dijkstra)