Dijkstra算法

6 阅读5分钟

算法概述

Dijkstra 用于在带权有向图中找到从源点到所有其他顶点的最短路径。要求边权非负。

核心思想

  1. 贪心策略:每次选择距离源点最近的未访问顶点
  2. 松弛操作:通过新顶点更新其他顶点的最短距离
  3. 逐步扩展:从源点开始,逐步扩展到所有顶点

算法步骤

  1. 初始化:源点距离为 0,其他顶点距离为无穷大
  2. 选择:从未访问顶点中选择距离最小的顶点 u
  3. 标记:将 u 标记为已访问
  4. 松弛:遍历 u 的邻接顶点,更新最短距离
  5. 重复:重复步骤 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;
}

算法执行过程示例

以示例图说明:

顶点: 01234
边:
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
10[0, 4, 2, ∞, ∞]访问0,更新1和2
22[0, 3, 2, 10, 12]访问2,更新1、3、4
31[0, 3, 2, 8, 12]访问1,更新3
43[0, 3, 2, 8, 10]访问3,更新4
54[0, 3, 2, 8, 10]访问4,完成

关键点说明

  1. 优先队列优化:使用最小堆快速获取距离最小的顶点
  2. 去重处理:优先队列中可能包含同一顶点的多个条目,需用 visited 数组去重
  3. 松弛操作:核心是 dist[v] = min(dist[v], dist[u] + weight)
  4. 路径重构:使用 prev 数组记录前驱,可回溯路径