图论入门:Dijkstra最短路径算法详解

前言

在现实生活中,我们经常会遇到这样的问题:从A地到B地,哪条路最近?在计算机网络中,数据包从源节点到目标节点,走哪条路径最快?这些问题本质上都是最短路径问题。而Dijkstra算法,就是解决这类问题最经典、最常用的算法之一。

一、什么是Dijkstra算法?

Dijkstra算法(又称迪杰斯特拉算法)是由荷兰计算机科学家Edsger W. Dijkstra在1956年提出、于1959年公开发表的一种经典算法。它用于解决单源最短路径问题——即从一个起点出发,找到该起点到图中所有其他顶点的最短路径。

简单来说:给定一个起点,Dijkstra算法能告诉你到图中每一个其他点最短要走多远

💡 单源的意思是只有一个起点。如果你想求任意两点之间的最短路径,可以多次运行Dijkstra算法(每个点作为起点跑一次)

算法的适用条件

Dijkstra算法有一个重要的前提:图中所有边的权重必须是非负数。如果存在负权边,Dijkstra算法可能会得到错误的结果。(对于含负权边的情况,可以使用Bellman-Ford算法。)

二、核心思想:贪心策略

Dijkstra算法的核心思想是贪心——每一步都选择当前看来"最优"的节点。

具体来说,算法将图中的顶点分为两组:

  • 已确定最短路径的顶点集合S:这些顶点的最短距离已经最终确定,不会再改变

  • 未确定最短路径的顶点集合U:这些顶点的最短距离还在更新中

算法每次从集合U中选出距离起点最近的那个顶点,将其移入集合S,然后尝试通过这个顶点去"松弛"更新其他顶点的距离。

🔑 松弛操作(Relaxation) :如果通过当前节点u能到达邻居v,且 起点到u的距离 + u到v的边的权重 < 起点到v的当前距离,那么就更新v的距离。简单说就是:"走u这条路会不会更近?"

三、算法步骤

Dijkstra算法的执行流程可以分为以下几个步骤:

第一步:初始化

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

  • 将起点的距离设为 0,其他所有顶点的距离设为 无穷大(∞)

  • 创建一个集合(或数组)记录哪些顶点已经被处理(已确定最短路径)

第二步:选择当前距离最小的未处理顶点

从所有未处理的顶点中,选出 dist[] 值最小的那个顶点 u。

第三步:松弛更新

遍历顶点 u 的所有邻居 v:

  • 如果 dist[u] + weight(u, v) < dist[v],则更新 dist[v] = dist[u] + weight(u, v)

  • 这表示"通过 u 走到 v 比之前记录的路径更短"

第四步:标记已处理

将顶点 u 标记为"已处理"(已确定最短路径),之后不再更新它。

第五步:重复

重复步骤二到步骤四,直到所有顶点都被处理完毕。

四、图解示例

4.1 示例图

本次使用的图包含 6 个顶点(A, B, C, D, E, F),所有边均为无向边(即可以双向通行),边上的数字代表权重(距离/成本)。

对应的边列表:

起点终点权重
AB3
AC1
AE1
BC2
BD4
BE3
CD1
CF1
DE6
DF3
EF6

4.2 算法执行过程

我们的起点为 A

初始状态
顶点ABCDEF
距离0
已处理

第 1 轮:处理 A

从 A 出发,查看 A 的所有邻居:

  • 到 B:距离 = 0 + 3 = 3

  • 到 C:距离 = 0 + 1 = 1

  • 到 E:距离 = 0 + 1 = 1

更新距离表:

顶点ABCDEF
距离0311
已处理

此时未处理顶点中距离最小的是 C(距离 1)和 E(距离 1)。我们以此来处理。


第 2 轮:处理 C

当前节点 C 的距离为 1。查看 C 的所有邻居(A, B, D, F):

  • 到 B:1 + 2 = 3,不大于现有的 3,不更新

  • 到 D:1 + 1 = 2,更新 D = 2

  • 到 F:1 + 1 = 2,更新 F = 2

顶点ABCDEF
距离031212
已处理

第 3 轮:处理 E

当前节点 E 的距离为 1。查看 E 的所有邻居(A, B, D, F):

  • 到 B:1 + 3 = 4,大于现有的 3,不更新

  • 到 D:1 + 6 = 7,大于现有的 2,不更新

  • 到 F:1 + 6 = 7,大于现有的 2,不更新

顶点ABCDEF
距离031212
已处理

第 4 轮:处理 D

当前节点 D 的距离为 2。查看 D 的所有邻居(B, C, E, F):

  • 到 B:2 + 4 = 6,大于现有的 3,不更新

  • 到 F:2 + 3 = 5,大于现有的 2,不更新

顶点ABCDEF
距离031212
已处理

第 5 轮:处理 F

当前节点 F 的距离为 2。查看 F 的所有邻居(C, D, E):

  • 到 C:2+1=3,大于1,不更新

  • 到 D:2+3=5,大于2,不更新

  • 到 E:2+6=8,大于1,不更新

顶点ABCDEF
距离031212
已处理

第 6 轮:处理 B

当前节点 B 的距离为 3。查看 B 的所有邻居(A, C, D, E):

  • 均不满足更新条件,算法结束。

4.3 最终结果

从 A 到各顶点的最短距离为:

顶点ABCDEF
最短距离031212

验证:A→D 的最短路径是 A → C → D(权重 1+1=2),而不是走 A→E→D(1+6=7)。✅

五、Java 代码实现

在实际应用中,我们通常使用优先队列(最小堆) 来优化"每次找距离最小的未处理顶点"这一操作,将时间复杂度从 O(V²) 降到 O((V+E)logV)。

5.1 完整代码

public class Dijkstra {

    /**
     * Dijkstra 算法(优先队列优化版)
     * @param n     顶点个数
     * @param graph 邻接表,graph.get(u) 返回 int[]{v, w} 数组,v是邻居,w是权重
     * @param start 起点编号
     * @return 从起点到各顶点的最短距离数组
     */
    public static int[] dijkstra(int n, List<List<int[]>> graph, int start) {
        // 1. 距离数组,初始化为无穷大
        int[] dist = new int[n];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[start] = 0;

        // 2. 优先队列(最小堆):存储 [节点编号, 当前距离]
        PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));    //队列里的所有包裹(节点),谁离起点的距离(a[1])小,谁就排在队头优先被弹出
        pq.offer(new int[]{start, 0});

        // 3. 标记数组:记录顶点是否已确定最短路径
        boolean[] visited = new boolean[n];

        while (!pq.isEmpty()) {
            // 弹出距离最小的节点
            int[] cur = pq.poll();
            int u = cur[0];//u是当前节点编号

            // 如果已经处理过,跳过(避免重复处理过时数据)
            if (visited[u]) continue;
            visited[u] = true;

            // 4. 遍历所有邻居,进行松弛操作
            for (int[] edge : graph.get(u)) {
                int v = edge[0];      // 邻居顶点
                int w = edge[1];      // 边权重

                // 如果通过 u 到 v 更短,则更新
                if (dist[u] + w < dist[v]) {
                    dist[v] = dist[u] + w;
                    pq.offer(new int[]{v, dist[v]});
                }
            }
        }

        return dist;
    }

    // 辅助方法:添加无向边(双向添加)
    private static void addEdge(List<List<int[]>> graph, int u, int v, int w) {
        graph.get(u).add(new int[]{v, w});
        graph.get(v).add(new int[]{u, w});
    }

    public static void main(String[] args) {
        // 顶点编号:A=0, B=1, C=2, D=3, E=4, F=5
        int n = 6;

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

        // 添加边(对应示例图)
        addEdge(graph, 0, 1, 3); // A-B
        addEdge(graph, 0, 2, 1); // A-C
        addEdge(graph, 0, 4, 1); // A-E
        addEdge(graph, 1, 2, 2); // B-C
        addEdge(graph, 1, 3, 4); // B-D
        addEdge(graph, 1, 4, 3); // B-E
        addEdge(graph, 2, 3, 1); // C-D
        addEdge(graph, 2, 5, 1); // C-F
        addEdge(graph, 3, 4, 6); // D-E
        addEdge(graph, 3, 5, 3); // D-F
        addEdge(graph, 4, 5, 6); // E-F

        // 运行算法,起点为 A(0)
        int[] result = dijkstra(n, graph, 0);

        // 打印结果
        char[] names = {'A', 'B', 'C', 'D', 'E', 'F'};
        for (int i = 0; i < n; i++) {
            System.out.println("到 " + names[i] + " 的最短距离为: " + result[i]);
        }
        // 输出:
        // 到 A 的最短距离为: 0
        // 到 B 的最短距离为: 3
        // 到 C 的最短距离为: 1
        // 到 D 的最短距离为: 2
        // 到 E 的最短距离为: 1
        // 到 F 的最短距离为: 2
    }
}

5.2 代码逐行解读

① 为什么用 Integer.MAX_VALUE 表示无穷大?
因为起点到其他点一开始未知,看作"无穷远",方便后面被更小的值替换。

② 优先队列里放 int[],它怎么排序的?

PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));

这行代码的意思是:队列里放的是 {节点, 距离},按照 距离(a[1]) 从小到大,每次 pq.poll() 拿到的都是距离最短的那个节点。

③ visited 数组是干什么的?
当某个节点的最短距离确定后,后面可能还会因为别的路径产生一个"较大的距离"被丢进队列(过时数据)。visited 确保每个节点只处理一次,一旦确定最短距离,就不再理它。

④ 核心三行"松弛操作":

java

if (dist[u] + w < dist[v]) {
    dist[v] = dist[u] + w;
    pq.offer(new int[]{v, dist[v]});
}
  • 当前站在 u,走到 u 已经花了 dist[u] 的距离

  • 如果从 u 再到 v,总花费 dist[u] + w 比之前记录的 dist[v] 更小,说明找到近路了!

  • 赶紧更新 dist[v],并把 v 丢进队列,等着用 v 去更新它的邻居

六、时间复杂度分析

Dijkstra算法有两种常见的实现方式:

实现方式时间复杂度适用场景
朴素实现(数组遍历)O(V²)稠密图(边多)
优先队列优化(二叉堆)O((V+E)logV)稀疏图(边少)

其中 V 是顶点数,E 是边数。

因为边少时优先队列省掉了朴素算法每次遍历全部顶点找最小值的巨额固定开销(O(V²)),而边多时优先队列需要为每条边支付一次昂贵的堆调整费用(O(logV)),累计成本(E·logV)超过了朴素算法仅做简单数组扫描和赋值的代价,所以边少用堆(省扫描费),边多用朴素(省堆调整费)。

对于大多数实际应用场景(如地图导航、网络路由),图通常是稀疏的,因此优先队列优化版本更为常用。

七、实际应用场景

Dijkstra算法的应用非常广泛,几乎涉及到所有需要"找最短/最快路径"的场景:

  • 地图导航:GPS设备使用Dijkstra算法计算从当前位置到目的地的最短或最快路线

  • 网络路由:OSPF(开放最短路径优先)协议就是基于Dijkstra算法来计算数据包的最优传输路径

  • 通信网络设计:规划基站、光纤等网络设施的最优布局

  • 机器人路径规划:让机器人在有障碍物的环境中找到从起点到终点的最优路径

  • 游戏AI:计算游戏中角色移动到目标位置的最短路径

  • 物流运输优化:规划配送路线,节省时间和成本

八、算法局限性

在使用Dijkstra算法之前,有几个重要的限制需要了解:

  1. 不能处理负权边:如果图中存在权重为负的边,Dijkstra算法可能给出错误的结果。对于这种情况,应该使用Bellman-Ford算法。

  2. 只能求单源最短路径:Dijkstra算法只能从一个起点出发,求到其他所有点的最短路径。

  3. 不能处理大规模动态图:如果图的结构频繁变化(如实时路况),每次重新运行Dijkstra算法的开销可能很大,需要考虑其他动态算法。

九、总结

让我们用一张流程图来回顾Dijkstra算法的核心流程:

text

        初始化
    (起点=0, 其他=∞)
          ↓
    从未处理节点中
    选出距离最小的u
          ↓
    通过u松弛更新邻居
          ↓
    标记u为已处理
          ↓
    所有节点已处理?
     ↙        ↘
    否         是
     ↖          ↓
      ─────  结束!

记住三个关键词:

  • 单源:从一个起点出发

  • 贪心:每次都选当前最近的节点

  • 松弛:通过已选节点去更新其他节点的距离

Dijkstra算法是图论中最基础也最重要的算法之一。掌握了它,你就拿到了解决最短路径问题的钥匙。无论是在算法竞赛、面试还是实际项目中,它都是一个高频出现的工具。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值