Java集合和数据结构-详解三大经典最短路径算法-Dijkstra (迪杰斯特拉) 算法、Bellman-Ford (贝尔曼-福特) 算法、Floyd-Warshall (弗洛伊德) 算法
本文系统介绍了三种经典的最短路径算法:Dijkstra算法适用于无负权边的单源最短路径问题,采用贪心策略,时间复杂度O(ElogV);Bellman-Ford算法能处理含负权边的情况并检测负权环,基于动态规划,时间复杂度O(VE);Floyd-Warshall算法解决所有顶点对的最短路径,采用动态规划思想,时间复杂度O(V³)。文章详细阐述了各算法的核心思想、实现细节(附Java代码)及适用场景,
好的,我们继续。根据您新图片中的内容,这次我们将深入探讨图论中另一个至关重要的话题:最短路径 (Shortest Path)。
我们将依次详细讲解三种经典的算法:
-
Dijkstra (迪杰斯特拉) 算法:用于解决无负权边的单源最短路径问题。
-
Bellman-Ford (贝尔曼-福特) 算法:用于解决可包含负权边的单源最短路径问题,并能检测负权环。
-
Floyd-Warshall (弗洛伊德) 算法:用于解决所有顶点对之间的最短路径问题(多源最短路径)。
1. 前置知识:最短路径问题
在一个带权图中,从一个顶点到另一个顶点的路径可能有多条。最短路径问题就是要找出一条路径,使得这条路径上所有边的权重之和最小。
-
路径权重 (Path Weight):一条路径上所有边的权重之和。
-
单源最短路径 (Single-Source Shortest Path, SSSP):计算从一个指定的**源顶点(Source)**到图中所有其他顶点的最短路径。Dijkstra 和 Bellman-Ford 解决的就是这类问题。
-
多源最短路径 (All-Pairs Shortest Path, APSP):计算图中每一对顶点之间的最短路径。Floyd-Warshall 解决的是这类问题。
-
负权边 (Negative-Weight Edge):边的权重可以为负数。这在现实中可能代表“收益”或“折扣”。Dijkstra 算法不能处理这种情况,而 Bellman-Ford 和 Floyd-Warshall 可以。
-
负权环 (Negative-Weight Cycle):一个环路的所有边权重之和为负数。如果图中存在负权环,那么最短路径可能没有意义,因为我们可以无限次地绕着这个环走,每走一圈路径权重就会变得更小,趋近于负无穷。Bellman-Ford 算法可以检测出这种情况。
2. Dijkstra (迪杰斯特拉) 算法 (5.1)
Dijkstra 算法是解决无负权边单源最短路径问题的最著名算法。它是一种贪心算法。
算法思想
-
初始化:
-
创建一个距离数组
dist[],dist[i]表示从源顶点s到顶点i的当前已知最短距离。初始化dist[s] = 0,其他所有dist[i] = ∞(无穷大)。 -
创建一个集合
S(或使用一个visited数组),用于存放已经找到最短路径的顶点。初始时S为空。
-
-
迭代:重复以下步骤 V 次(V 是顶点数):
-
从尚未访问的顶点中,选择一个
dist值最小的顶点u。 -
将
u加入集合S(标记为已访问)。这表示从源点s到u的最短路径已经确定为dist[u]。 -
松弛 (Relaxation):对于
u的每一个邻接顶点v,检查是否可以通过u来缩短到达v的路径。即,比较dist[v]和dist[u] + weight(u, v):-
如果
dist[u] + weight(u, v) < dist[v],则更新dist[v] = dist[u] + weight(u, v)。
-
-
核心数据结构:优先队列 (Min-Heap)
为了在每次迭代中高效地“选择一个 dist 值最小的尚未访问的顶点”,我们使用优先队列。
-
优先队列中存储
(距离, 顶点)对。 -
每次从队列顶部取出距离最小的顶点,这个操作的时间复杂度是 O(logV)。
Java 代码实现与逐行注释
Java
import java.util.*;
// 用于在优先队列中存储节点信息
class Node implements Comparable<Node> {
int vertex; // 顶点编号
int distance; // 从源点到该顶点的距离
public Node(int vertex, int distance) {
this.vertex = vertex;
this.distance = distance;
}
// 实现 Comparable 接口,让优先队列能按 distance 排序
@Override
public int compareTo(Node other) {
return this.distance - other.distance; // 按距离升序排列
}
}
public class DijkstraAlgorithm {
// Dijkstra 算法实现
// V: 顶点数, adj: 邻接表, src: 源顶点
public static void dijkstra(int V, List<List<Node>> adj, int src) {
// dist 数组:存储从源点 src 到各个顶点的最短距离
int[] dist = new int[V];
// 初始化所有距离为无穷大
Arrays.fill(dist, Integer.MAX_VALUE);
// 源点到自身的距离为 0
dist[src] = 0;
// 优先队列(最小堆),用于高效地获取当前距离最小的顶点
// 存储的是 Node 对象,包含顶点和其当前距离
PriorityQueue<Node> pq = new PriorityQueue<>();
// 将源点加入优先队列
pq.add(new Node(src, 0));
System.out.println("Dijkstra 算法执行过程:");
// 当优先队列不为空时,说明还有未处理的顶点
while (!pq.isEmpty()) {
// 1. 从优先队列中取出距离源点最近的顶点 u
Node currentNode = pq.poll();
int u = currentNode.vertex;
int currentDist = currentNode.distance;
// 这是一个优化:如果取出的距离比数组中记录的还要大,
// 说明这个节点信息是“过时”的(我们找到了更短的路径),直接跳过。
if (currentDist > dist[u]) {
continue;
}
System.out.printf("处理顶点 %d,当前最短距离为 %d\n", u, dist[u]);
// 2. 遍历 u 的所有邻接顶点 v (松弛操作)
for (Node neighbor : adj.get(u)) {
int v = neighbor.vertex;
int weight = neighbor.distance; // 这里 distance 存储的是边的权重
// 3. 如果通过 u 到达 v 的距离比已知的 dist[v] 更短
if (dist[u] + weight < dist[v]) {
// 更新到 v 的最短距离
dist[v] = dist[u] + weight;
// 将更新后的 (v, dist[v]) 加入优先队列
pq.add(new Node(v, dist[v]));
System.out.printf(" -> 松弛边 (%d, %d),更新 dist[%d] = %d\n", u, v, v, dist[v]);
}
}
}
// 打印最终结果
System.out.println("\n从源点 " + src + " 到各顶点的最短路径:");
for (int i = 0; i < V; i++) {
System.out.printf("顶点 %d: %s\n", i, (dist[i] == Integer.MAX_VALUE ? "不可达" : dist[i]));
}
}
public static void main(String[] args) {
int V = 5; // 顶点数
int src = 0; // 源点
// 使用邻接表表示图,List<Node> 表示与该顶点相连的边
List<List<Node>> adj = new ArrayList<>(V);
for (int i = 0; i < V; i++) {
adj.add(new ArrayList<>());
}
// 添加边和权重
adj.get(0).add(new Node(1, 9));
adj.get(0).add(new Node(2, 6));
adj.get(0).add(new Node(3, 5));
adj.get(0).add(new Node(4, 3));
adj.get(2).add(new Node(1, 2));
adj.get(2).add(new Node(3, 4));
dijkstra(V, adj, src);
}
}
复杂度与扩展
-
时间复杂度:O(ElogV)。E 是边数,V 是顶点数。每个顶点最多入队和出队一次 (O(VlogV)),每条边最多被松弛一次 (O(ElogV))。
-
空间复杂度:O(V+E)。
-
局限性:不能处理带负权边的图。因为 Dijkstra 算法的贪心策略是基于“一旦一个顶点的最短路径被确定,它就不会再被更改”。但如果存在负权边,一条更长的路径(经过更多顶点)的总权重反而可能更小,这会打破 Dijkstra 算法的前提。
-
应用:路由协议(如 OSPF)、地图导航(计算最快或最短路线)、网络数据包转发等。
3. Bellman-Ford (贝尔曼-福特) 算法 (5.2)
Bellman-Ford 算法同样解决单源最短路径问题,但它更强大,因为可以处理带负权边的图,并且能够检测负权环。
算法思想
其核心思想是基于动态规划和松弛操作。它认为从源点 s 到任意顶点 t 的最短路径,最多包含 V−1 条边(如果超过,则一定有环)。
-
初始化:和 Dijkstra 一样,初始化
dist数组,dist[s] = 0,其他为∞。 -
迭代松弛:重复 V−1 次以下操作:
-
遍历图中的所有边
(u, v),并对它们进行松弛操作。 -
if (dist[u] + weight(u, v) < dist[v]),则更新dist[v] = dist[u] + weight(u, v)。
-
-
检测负权环:在完成上述 V−1 轮迭代后,再进行第 V 轮迭代:
-
再次遍历所有边
(u, v)进行松弛。 -
如果在这一轮中,仍然有
dist[v]的值被更新,则说明图中存在一个从源点可达的负权环。
-
Java 代码实现与逐行注释
Java
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
// 边类,用于 Bellman-Ford 的边列表表示
class Edge {
int src, dest, weight;
public Edge(int src, int dest, int weight) {
this.src = src;
this.dest = dest;
this.weight = weight;
}
}
public class BellmanFordAlgorithm {
public static void bellmanFord(int V, List<Edge> edges, int src) {
// dist 数组:存储从源点 src 到各个顶点的最短距离
int[] dist = new int[V];
// 初始化所有距离为无穷大
Arrays.fill(dist, Integer.MAX_VALUE);
// 源点到自身的距离为 0
dist[src] = 0;
// 步骤 1: 进行 V-1 轮松弛操作
// 因为最短路径最多包含 V-1 条边
for (int i = 1; i < V; i++) {
System.out.println("第 " + i + " 轮松弛:");
// 遍历图中的每一条边
for (Edge edge : edges) {
int u = edge.src;
int v = edge.dest;
int weight = edge.weight;
// 如果起点 u 是可达的,并且通过 u 到达 v 的路径更短
if (dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
System.out.printf(" -> 松弛边 (%d, %d),更新 dist[%d] = %d\n", u, v, v, dist[v]);
}
}
}
// 步骤 2: 进行第 V 轮,用于检测负权环
boolean hasNegativeCycle = false;
System.out.println("\n进行第 " + V + " 轮以检测负权环:");
for (Edge edge : edges) {
int u = edge.src;
int v = edge.dest;
int weight = edge.weight;
// 如果在第 V 轮仍然可以松弛,说明存在负权环
if (dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {
hasNegativeCycle = true;
System.out.printf(" -> 边 (%d, %d) 仍然可以被松弛,发现负权环!\n", u, v);
break; // 找到一个即可停止
}
}
// 打印最终结果
if (hasNegativeCycle) {
System.out.println("\n图中存在从源点可达的负权环,无法计算最短路径。");
} else {
System.out.println("\n从源点 " + src + " 到各顶点的最短路径:");
for (int i = 0; i < V; i++) {
System.out.printf("顶点 %d: %s\n", i, (dist[i] == Integer.MAX_VALUE ? "不可达" : dist[i]));
}
}
}
public static void main(String[] args) {
int V = 5;
int src = 0;
List<Edge> edges = new ArrayList<>();
edges.add(new Edge(0, 1, -1));
edges.add(new Edge(0, 2, 4));
edges.add(new Edge(1, 2, 3));
edges.add(new Edge(1, 3, 2));
edges.add(new Edge(1, 4, 2));
edges.add(new Edge(3, 2, 5));
edges.add(new Edge(3, 1, 1));
edges.add(new Edge(4, 3, -3));
bellmanFord(V, edges, src);
// 包含负权环的例子
System.out.println("\n--- 测试包含负权环的图 ---");
V = 3;
src = 0;
edges.clear();
edges.add(new Edge(0, 1, 1));
edges.add(new Edge(1, 2, -1));
edges.add(new Edge(2, 1, -1)); // 1->2->1 形成负权环
bellmanFord(V, edges, src);
}
}
复杂度与扩展
-
时间复杂度:O(V⋅E)。外层循环 V−1 次,内层循环遍历所有 E 条边。
-
空间复杂度:O(V+E)。
-
优点:非常稳健,可以处理负权边,还能检测负权环。
-
应用:在网络路由协议 RIP (Routing Information Protocol) 中有所应用。在金融领域,可用于检测套利机会(这可以模型化为寻找负权环)。
4. Floyd-Warshall (弗洛伊德) 算法 (5.3)
Floyd-Warshall 算法解决的是所有顶点对之间的最短路径(APSP)问题。它同样可以处理负权边,但不能处理负权环。
算法思想
这是一个非常优美的动态规划算法。其核心思想是:
-
对于任意一对顶点
(i, j),它们之间的最短路径,要么是直接连接它们的边(i, j),要么是经过了某个中间顶点k。 -
算法通过一个
k的循环,逐步允许更多的顶点作为中间点,从而不断优化任意两点i和j之间的路径。
状态转移方程为:
dist(i,j)=min(dist(i,j),dist(i,k)+dist(k,j))
这个方程的含义是:从 i 到 j 的新最短路径,是“原来的最短路径”和“经过 k 中转的路径”两者中的较小值。
Java 代码实现与逐行注释
Java
import java.util.Arrays;
public class FloydWarshallAlgorithm {
// 定义无穷大,防止加法溢出
final static int INF = 99999;
public static void floydWarshall(int V, int[][] graph) {
// dist 矩阵:用于存储任意两点间的最短路径
// 初始化 dist 矩阵等于输入的邻接矩阵
int[][] dist = new int[V][V];
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
dist[i][j] = graph[i][j];
}
}
// 核心的动态规划过程
// k 是允许作为中间点的顶点
for (int k = 0; k < V; k++) {
System.out.println("允许使用顶点 " + k + " 作为中间点进行更新:");
// i 是路径的起点
for (int i = 0; i < V; i++) {
// j 是路径的终点
for (int j = 0; j < V; j++) {
// 如果从 i 到 k 和从 k 到 j 的路径都存在
if (dist[i][k] != INF && dist[k][j] != INF) {
// 检查通过 k 中转是否能缩短 i 到 j 的距离
if (dist[i][k] + dist[k][j] < dist[i][j]) {
// 更新 dist[i][j]
dist[i][j] = dist[i][k] + dist[k][j];
System.out.printf(" -> 更新 dist[%d][%d] = dist[%d][%d] + dist[%d][%d] = %d\n",
i, j, i, k, k, j, dist[i][j]);
}
}
}
}
}
// 打印最终的最短路径矩阵
printSolution(V, dist);
// 检测负权环:如果一个顶点到其自身的距离为负,则存在负权环
for (int i = 0; i < V; i++) {
if (dist[i][i] < 0) {
System.out.println("\n图中存在负权环!");
return;
}
}
}
public static void printSolution(int V, int[][] dist) {
System.out.println("\n所有顶点对之间的最短路径矩阵:");
for (int i = 0; i < V; ++i) {
for (int j = 0; j < V; ++j) {
if (dist[i][j] == INF) {
System.out.print("INF ");
} else {
System.out.print(dist[i][j] + " ");
}
}
System.out.println();
}
}
public static void main(String[] args) {
int V = 4;
// 使用邻接矩阵表示图
int[][] graph = {
{0, 5, INF, 10},
{INF, 0, 3, INF},
{INF, INF, 0, 1},
{INF, INF, INF, 0}
};
floydWarshall(V, graph);
}
}
复杂度与扩展
-
时间复杂度:O(V3)。由于三层嵌套循环,非常直观。
-
空间复杂度:O(V2)。用于存储距离矩阵。
-
优点:代码简洁,易于实现。对于需要计算所有点对之间最短路径的稠密图来说,效率很高。
-
应用:计算图的传递闭包、寻找图的中心点、交通网络中的路线规划等。
总结与对比
| 算法 | 问题类型 | 边权要求 | 时间复杂度 | 核心思想 |
| Dijkstra | 单源最短路径 (SSSP) | 不能有负权边 | O(ElogV) | 贪心法,每次选择最近的未访问顶点 |
| Bellman-Ford | 单源最短路径 (SSSP) | 可以有负权边 | O(V⋅E) | 动态规划,迭代松弛所有边 V−1 次 |
| Floyd-Warshall | 多源最短路径 (APSP) | 可以有负权边 (但不能有负权环) | O(V3) | 动态规划,不断引入中间点优化路径 |
希望这份详细的讲解能帮助您彻底理解这三种核心的最短路径算法!
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)