算法概述
Dijkstra 用于在带权有向图中找到从源点到所有其他顶点的最短路径。要求边权非负。
核心思想
- 贪心策略:每次选择距离源点最近的未访问顶点
- 松弛操作:通过新顶点更新其他顶点的最短距离
- 逐步扩展:从源点开始,逐步扩展到所有顶点
算法步骤
- 初始化:源点距离为 0,其他顶点距离为无穷大
- 选择:从未访问顶点中选择距离最小的顶点 u
- 标记:将 u 标记为已访问
- 松弛:遍历 u 的邻接顶点,更新最短距离
- 重复:重复步骤 2-4,直到所有顶点被访问
时间复杂度
- 使用优先队列(堆):O((V + E) log V)
- 使用普通数组:O(V²)
- V 为顶点数,E 为边数
适用场景
- 单源最短路径问题
- 边权非负
- 有向图或无向图
Java 实现
提供两种实现:基础版本(数组)和优化版本(优先队列)。
版本一:使用优先队列(推荐)
import java.util.*;
/**
* Dijkstra算法实现 - 使用优先队列优化版本
* 用于求解单源最短路径问题
*/
public class Dijkstra {
/**
* 图的邻接表表示
* graph[i] 是一个列表,包含从顶点i出发的所有边
* 每条边用 int[]{目标顶点, 权重} 表示
*/
private List<List<int[]>> graph;
private int n; // 顶点数量
/**
* 构造函数
* @param n 顶点数量(顶点编号从0到n-1)
*/
public Dijkstra(int n) {
this.n = n;
this.graph = new ArrayList<>();
for (int i = 0; i < n; i++) {
graph.add(new ArrayList<>());
}
}
/**
* 添加边
* @param from 起始顶点
* @param to 目标顶点
* @param weight 边的权重(必须非负)
*/
public void addEdge(int from, int to, int weight) {
graph.get(from).add(new int[]{to, weight});
}
/**
* Dijkstra算法主方法
* 使用优先队列(最小堆)优化,时间复杂度 O((V+E)logV)
*
* @param start 源点
* @return 从源点到所有顶点的最短距离数组,dist[i]表示从start到i的最短距离
*/
public int[] dijkstra(int start) {
// 距离数组,dist[i]表示从start到i的最短距离
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[start] = 0;
// 优先队列,按距离从小到大排序 [顶点, 距离]
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);
pq.offer(new int[]{start, 0});
// 已访问顶点集合
boolean[] visited = new boolean[n];
while (!pq.isEmpty()) {
// 取出距离最小的顶点
int[] current = pq.poll();
int u = current[0];
int distance = current[1];
// 如果该顶点已访问过,跳过(优先队列中可能有重复的顶点)
if (visited[u]) {
continue;
}
// 标记为已访问
visited[u] = true;
// 遍历u的所有邻接顶点
for (int[] edge : graph.get(u)) {
int v = edge[0]; // 目标顶点
int weight = edge[1]; // 边的权重
// 松弛操作:如果通过u到达v的距离更短,则更新
if (!visited[v] && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
pq.offer(new int[]{v, dist[v]});
}
}
}
return dist;
}
/**
* 获取最短路径(不仅返回距离,还返回路径)
* @param start 源点
* @param end 终点
* @return 从start到end的最短路径列表,如果不可达返回null
*/
public List<Integer> getShortestPath(int start, int end) {
// 距离数组
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[start] = 0;
// 前驱数组,用于记录路径
int[] prev = new int[n];
Arrays.fill(prev, -1);
// 优先队列
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);
pq.offer(new int[]{start, 0});
boolean[] visited = new boolean[n];
while (!pq.isEmpty()) {
int[] current = pq.poll();
int u = current[0];
if (visited[u]) {
continue;
}
visited[u] = true;
// 如果已经到达终点,可以提前结束
if (u == end) {
break;
}
for (int[] edge : graph.get(u)) {
int v = edge[0];
int weight = edge[1];
if (!visited[v] && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
prev[v] = u; // 记录前驱
pq.offer(new int[]{v, dist[v]});
}
}
}
// 如果终点不可达
if (dist[end] == Integer.MAX_VALUE) {
return null;
}
// 重构路径
List<Integer> path = new ArrayList<>();
int current = end;
while (current != -1) {
path.add(current);
current = prev[current];
}
Collections.reverse(path);
return path;
}
/**
* 测试方法
*/
public static void main(String[] args) {
// 创建图:5个顶点
Dijkstra dijkstra = new Dijkstra(5);
// 添加边(有向图)
// 0 -> 1 (权重4)
dijkstra.addEdge(0, 1, 4);
// 0 -> 2 (权重2)
dijkstra.addEdge(0, 2, 2);
// 1 -> 2 (权重1)
dijkstra.addEdge(1, 2, 1);
// 1 -> 3 (权重5)
dijkstra.addEdge(1, 3, 5);
// 2 -> 1 (权重3)
dijkstra.addEdge(2, 1, 3);
// 2 -> 3 (权重8)
dijkstra.addEdge(2, 3, 8);
// 2 -> 4 (权重10)
dijkstra.addEdge(2, 4, 10);
// 3 -> 4 (权重2)
dijkstra.addEdge(3, 4, 2);
// 4 -> 3 (权重1)
dijkstra.addEdge(4, 3, 1);
// 从顶点0开始计算最短路径
int start = 0;
int[] distances = dijkstra.dijkstra(start);
System.out.println("从顶点 " + start + " 到各顶点的最短距离:");
for (int i = 0; i < distances.length; i++) {
if (distances[i] == Integer.MAX_VALUE) {
System.out.println("顶点 " + i + ": 不可达");
} else {
System.out.println("顶点 " + i + ": " + distances[i]);
}
}
// 获取从0到4的最短路径
System.out.println("\n从顶点 0 到顶点 4 的最短路径:");
List<Integer> path = dijkstra.getShortestPath(0, 4);
if (path != null) {
System.out.println("路径: " + path);
System.out.println("距离: " + distances[4]);
} else {
System.out.println("不可达");
}
}
}
版本二:使用数组(适合稠密图)
/**
* Dijkstra算法实现 - 使用数组版本
* 适合稠密图,时间复杂度 O(V²)
*/
public int[] dijkstraArray(int start) {
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[start] = 0;
boolean[] visited = new boolean[n];
// 循环n次,每次找到一个最短路径
for (int i = 0; i < n; i++) {
// 找到未访问顶点中距离最小的
int u = -1;
int minDist = Integer.MAX_VALUE;
for (int j = 0; j < n; j++) {
if (!visited[j] && dist[j] < minDist) {
minDist = dist[j];
u = j;
}
}
// 如果所有顶点都已访问或不可达,结束
if (u == -1) {
break;
}
visited[u] = true;
// 松弛操作
for (int[] edge : graph.get(u)) {
int v = edge[0];
int weight = edge[1];
if (!visited[v] && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
}
}
}
return dist;
}
算法执行过程示例
以示例图说明:
顶点: 0, 1, 2, 3, 4
边:
0 -> 1 (4)
0 -> 2 (2)
1 -> 2 (1)
1 -> 3 (5)
2 -> 1 (3)
2 -> 3 (8)
2 -> 4 (10)
3 -> 4 (2)
4 -> 3 (1)
从顶点 0 开始执行过程:
| 步骤 | 当前顶点 | 距离数组 [0,1,2,3,4] | 说明 |
|---|---|---|---|
| 初始 | - | [0, ∞, ∞, ∞, ∞] | 源点距离为0 |
| 1 | 0 | [0, 4, 2, ∞, ∞] | 访问0,更新1和2 |
| 2 | 2 | [0, 3, 2, 10, 12] | 访问2,更新1、3、4 |
| 3 | 1 | [0, 3, 2, 8, 12] | 访问1,更新3 |
| 4 | 3 | [0, 3, 2, 8, 10] | 访问3,更新4 |
| 5 | 4 | [0, 3, 2, 8, 10] | 访问4,完成 |
关键点说明
- 优先队列优化:使用最小堆快速获取距离最小的顶点
- 去重处理:优先队列中可能包含同一顶点的多个条目,需用 visited 数组去重
- 松弛操作:核心是 dist[v] = min(dist[v], dist[u] + weight)
- 路径重构:使用 prev 数组记录前驱,可回溯路径