Langchain系列文章目录

01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来

Python系列文章目录

PyTorch系列文章目录

机器学习系列文章目录

深度学习系列文章目录

Java系列文章目录

JavaScript系列文章目录

Python系列文章目录

Go语言系列文章目录

Docker系列文章目录

数据结构与算法系列文章目录

01-【数据结构与算法-Day 1】程序世界的基石:到底什么是数据结构与算法?
02-【数据结构与算法-Day 2】衡量代码的标尺:时间复杂度与大O表示法入门
03-【数据结构与算法-Day 3】揭秘算法效率的真相:全面解析O(n^2), O(2^n)及最好/最坏/平均复杂度
04-【数据结构与算法-Day 4】从O(1)到O(n²),全面掌握空间复杂度分析
05-【数据结构与算法-Day 5】实战演练:轻松看懂代码的时间与空间复杂度
06-【数据结构与算法-Day 6】最朴素的容器 - 数组(Array)深度解析
07-【数据结构与算法-Day 7】告别数组束缚,初识灵活的链表 (Linked List)
08-【数据结构与算法-Day 8】手把手带你拿捏单向链表:增、删、改核心操作详解
09-【数据结构与算法-Day 9】图解单向链表:从基础遍历到面试必考的链表反转
10-【数据结构与算法-Day 10】双向奔赴:深入解析双向链表(含图解与代码)
11-【数据结构与算法-Day 11】从循环链表到约瑟夫环,一文搞定链表的终极形态
12-【数据结构与算法-Day 12】深入浅出栈:从“后进先出”原理到数组与链表双实现
13-【数据结构与算法-Day 13】栈的应用:从括号匹配到逆波兰表达式求值,面试高频考点全解析
14-【数据结构与算法-Day 14】先进先出的公平:深入解析队列(Queue)的核心原理与数组实现
15-【数据结构与算法-Day 15】告别“假溢出”:深入解析循环队列与双端队列
16-【数据结构与算法-Day 16】队列的应用:广度优先搜索(BFS)的基石与迷宫寻路实战
17-【数据结构与算法-Day 17】揭秘哈希表:O(1)查找速度背后的魔法
18-【数据结构与算法-Day 18】面试必考!一文彻底搞懂哈希冲突四大解决方案:开放寻址、拉链法、再哈希
19-【数据结构与算法-Day 19】告别线性世界,一文掌握树(Tree)的核心概念与表示法
20-【数据结构与算法-Day 20】从零到一掌握二叉树:定义、性质、特殊形态与存储结构全解析
21-【数据结构与算法-Day 21】精通二叉树遍历(上):前序、中序、后序的递归与迭代实现
22-【数据结构与算法-Day 22】玩转二叉树遍历(下):广度优先搜索(BFS)与层序遍历的奥秘
23-【数据结构与算法-Day 23】为搜索而生:一文彻底搞懂二叉搜索树 (BST) 的奥秘
24-【数据结构与算法-Day 24】平衡的艺术:图解AVL树,彻底告别“瘸腿”二叉搜索树
25-【数据结构与算法-Day 25】工程中的王者:深入解析红黑树 (Red-Black Tree)
26-【数据结构与算法-Day 26】堆:揭秘优先队列背后的“特殊”完全二叉树
27-【数据结构与算法-Day 27】堆的应用:从堆排序到 Top K 问题,一文彻底搞定!
28-【数据结构与算法-Day 28】字符串查找的终极利器:深入解析字典树 (Trie / 前缀树)
29-【数据结构与算法-Day 29】从社交网络到地图导航,一文带你入门终极数据结构:图
30-【数据结构与算法-Day 30】图的存储:邻接矩阵 vs 邻接表,哪种才是最优选?
31-【数据结构与算法-Day 31】图的遍历:深度优先搜索 (DFS) 详解,一条路走到黑的智慧
32-【数据结构与算法-Day 32】掌握广度优先搜索 (BFS),轻松解决无权图最短路径问题
33-【数据结构与算法-Day 33】最小生成树之 Prim 算法:从零构建通信网络
34-【数据结构与算法-Day 34】最小生成树之 Kruskal 算法:从边的视角构建最小网络
35-【数据结构与算法-Day 35】拓扑排序:从依赖关系到关键路径的完整解析
36-【数据结构与算法-Day 36】查找算法入门:从顺序查找的朴素到二分查找的惊艳
37-【数据结构与算法-Day 37】超越二分查找:探索插值、斐波那契与分块查找的奥秘
38-【数据结构与算法-Day 38】排序算法入门:图解冒泡排序与选择排序,从零掌握 O(n²) 经典思想
39-【数据结构与算法-Day 39】插入排序与希尔排序:从 O(n²) 到 O(n^1.3) 的性能飞跃
40-【数据结构与算法-Day 40】分治思想:化繁为简的“分而治之”编程艺术
41-【数据结构与算法-Day 41】分治之王:深入解析稳定高效的归并排序
42-【数据结构与算法-Day 42】快速排序(Quick Sort)入门:从 partition 分区操作到递归实现
43-【数据结构与算法-Day 43】深入剖析快速排序:随机化、三路快排与工程应用
44-【数据结构与算法-Day 44】线性时间排序的奥秘:一文搞懂计数排序与桶排序
45-【数据结构与算法-Day 45】超越比较的极限:详解非比较排序之王——基数排序
46-【数据结构与算法-Day 46】图解最短路径:Dijkstra算法从原理到实战(无负权边)



摘要

在纷繁复杂的网络世界中,如何找到从一个点到另一个点的最短路径,是计算机科学领域一个经典且至关重要的问题。无论是GPS导航规划最快路线,还是网络路由器选择最高效的数据传输路径,其背后都离不开最短路径算法的支持。本文将深入探讨解决单源最短路径问题的经典算法——Dijkstra(迪杰斯特拉)算法。我们将从其核心的贪心思想出发,通过生动的图解和实例演练,逐步揭示其工作原理。此外,文章还将提供从朴素实现到优先队列优化的两种核心代码实现(C++/Java),并分析其性能差异,最后探讨其适用场景与局限性。无论你是算法初学者还是希望巩固知识的进阶者,本文都将为你构建一个关于Dijkstra算法的清晰知识体系。

一、引言:最短路径问题是什么?

在正式进入算法学习之前,我们首先需要明确我们要解决的问题是什么。

1.1 生活中的最短路径

想象一下,你正计划一场自驾游,从“城市A”出发,希望到达“城市D”。途中有多个城市可以中转,每两个城市之间的道路都有不同的距离。你手上的地图就是一个“图”,城市是“顶点”,道路是“边”,道路的长度就是“权重”。你的目标就是找到一条从A到D的总距离最短的路线。

这就是一个典型的最短路径问题。它不仅仅局限于地理距离,也可以是时间、费用、或者网络延迟等任何可以量化的成本。

1.2 问题的形式化定义

在计算机科学中,我们将这个问题进行抽象和形式化:

  • 图 (Graph):一个图由顶点集合 V V V 和边集合 E E E 构成,记为 G = ( V , E ) G=(V, E) G=(V,E)
  • 带权图 (Weighted Graph):图中的每条边 ( u , v ) (u, v) (u,v) 都关联一个数值,称为权重 w ( u , v ) w(u, v) w(u,v),代表从顶点 u u u 到顶点 v v v 的成本。
  • 路径 (Path):从一个顶点到另一个顶点经过的边序列。
  • 路径长度 (Path Length):一条路径上所有边的权重之和。
  • 单源最短路径 (Single-Source Shortest Path, SSSP):给定一个图 G G G 和一个源顶点 s s s,找出从 s s s 到图中所有其他顶点的最短路径。

Dijkstra算法正是解决带权有向图或无向图中,边权为非负数时的单源最短路径问题的利器。

二、Dijkstra 算法的核心思想:贪心的智慧

Dijkstra算法由荷兰计算机科学家艾兹赫尔·迪杰斯特拉在1956年提出,其核心是一种贪心策略 (Greedy Strategy)

2.1 算法的直观理解

我们可以用一个“感染”模型来直观理解Dijkstra算法。

  1. 初始状态:只有源顶点 s s s 是“已确定最短路径”的(可以看作是第一个“感染者”),它到自身的距离为0。其他所有顶点都处于“未确定”状态,距离源点的距离被初始化为无穷大。
  2. 扩张过程:算法每次都从“未确定”的顶点中,选择一个距离源顶点 s s s 最近的顶点 u u u
  3. 确定与更新:一旦选择了顶点 u u u,我们就贪心地认为:从源点 s s s u u u 的当前最短路径,就是最终的最短路径了。因此,将 u u u 标记为“已确定”。然后,通过 u u u 这个新确定的“跳板”,去看看能否找到到达 u u u 的邻居们更近的路径,这个过程称为“松弛”。
  4. 循环往复:重复第2、3步,直到所有顶点都被标记为“已确定”,或者所有从源点可达的顶点都已处理完毕。

这个过程就像一个以源点为中心的不断扩大的圈,每次都将离中心最近的点纳入圈内,并更新圈外邻近点的信息。

2.2 核心三要素

为了实现上述思想,我们需要三个关键的数据结构:

2.2.1 dist 数组:距离记录本

一个数组(或哈希表),dist[v] 记录了从源顶点 s s s 到顶点 v v v当前已知的最短路径长度。

  • 初始化:dist[s] = 0,对于所有其他顶点 v ≠ s v \ne s v=sdist[v] = \infty
  • 更新:在算法执行过程中,dist 数组的值会不断被优化,变得越来越小,直到最终确定。

2.2.2 visited 集合:已确定最短路径的顶点

一个集合(通常用布尔数组实现),visited[v] 用于标记顶点 v v v 是否已经找到了从源点 s s s 出发的最短路径。

  • 初始化:所有顶点都未被访问,visited 集合为空。
  • 操作:每次从dist数组中选出值最小且未被访问的顶点后,就将其加入 visited 集合。

2.2.3 松弛 (Relaxation) 操作:更新最短路径

这是算法的核心操作。当我们考察一个新确定的顶点 u u u 和它的一个邻居 v v v 时,我们会检查是否存在一条更短的路径:源点 s s s -> … -> u u u -> v v v
如果 dist[u] (从 s s s u u u 的已知最短距离) 加上边 ( u , v ) (u, v) (u,v) 的权重 w ( u , v ) w(u, v) w(u,v),小于 dist[v] (从 s s s v v v 的已知最短距离),那么我们就找到了到达 v v v 的一条更短路径。

其伪代码可以表示为:

if dist[u] + weight(u, v) < dist[v] then
    dist[v] = dist[u] + weight(u, v)

这个判断和更新的过程就叫做松弛

三、算法执行步骤详解

现在,我们将上述思想和要素整合成一个清晰的算法流程。

3.1 初始化

  1. 创建一个 dist 数组,将源顶点 s 的距离 dist[s] 初始化为 0,其他所有顶点的距离初始化为无穷大 ( ∞ \infty )。
  2. 创建一个 visited 布尔数组,所有元素初始化为 false

3.2 迭代过程(循环 V 次)

对于一个包含 V 个顶点的图,循环 V 次:

3.2.1 选点:选择 dist 值最小的未访问顶点 u

在所有 visited[i]false 的顶点中,找到一个使 dist[i] 最小的顶点,记为 u

3.2.2 标记:将 u 加入 visited 集合

u 标记为已访问,即 visited[u] = true。这代表源点到 u 的最短路径已经被找到,就是 dist[u]

3.2.3 松弛:更新 u 的所有邻接点

遍历 u 的所有邻接顶点 v

  • 如果 v 尚未被访问 (visited[v] == false),并且通过 u 到达 v 的路径更短(即 dist[u] + w(u, v) < dist[v]),则执行松弛操作,更新 dist[v] = dist[u] + w(u, v)

3.3 流程图可视化

下面是 Dijkstra 算法的执行流程图:

渲染错误: Mermaid 渲染失败: Parse error on line 3: ... B --> C{循环 V 次 (V 为顶点数)}; C --> ----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'

四、Dijkstra 算法的实例演练

纸上得来终觉浅,我们通过一个具体的例子来走一遍Dijkstra的完整流程。

4.1 示例图

假设我们有以下带权有向图,并选择顶点 0 作为源点。

示例图

10

3

1

8

4

8

2

5

0

1

2

3

4

4.2 分步图解

初始状态:

  • dist = [0, ∞ \infty , ∞ \infty , ∞ \infty , ∞ \infty ]
  • visited = [F, F, F, F, F] (T=True, F=False)
迭代次数visited 集合选择的顶点 udist 数组状态描述
-{}-[0, inf, inf, inf, inf]初始化。源点0到自身距离为0。
1{}0 (dist=0)[0, 10, 3, inf, inf]选择dist最小的未访问顶点0。标记为已访问。松弛邻居1和2:dist[1]=10, dist[2]=3
2{0}2 (dist=3)[0, 7, 3, 11, 5]在未访问的{1,2,3,4}中,2dist最小(3)。标记2。松弛其邻居1,3,4:dist[0]+d(0,2)+d(2,1)=3+4=7 < 10 -> dist[1]=7d(0,2)+d(2,3)=3+8=11 -> dist[3]=11d(0,2)+d(2,4)=3+2=5 -> dist[4]=5
3{0, 2}4 (dist=5)[0, 7, 3, 11, 5]{1,3,4}中,4dist最小(5)。标记4。4没有未访问的邻居,不进行松弛。
4{0, 2, 4}1 (dist=7)[0, 7, 3, 8, 5]{1,3}中,1dist最小(7)。标记1。松弛其邻居3:dist[1]+d(1,3)=7+1=8 < 11 -> dist[3]=8
5{0, 2, 4, 1}3 (dist=8)[0, 7, 3, 8, 5]只剩3未访问,选择3。标记3。其邻居4已访问,不进行松弛。

最终结果:
算法结束。dist 数组为 [0, 7, 3, 8, 5],这代表从源点0到各顶点的最短路径长度分别为:

  • 0 -> 0: 0
  • 0 -> 1: 7 (路径 0 -> 2 -> 1)
  • 0 -> 2: 3 (路径 0 -> 2)
  • 0 -> 3: 8 (路径 0 -> 2 -> 1 -> 3)
  • 0 -> 4: 5 (路径 0 -> 2 -> 4)

五、代码实现:从朴素到优化

下面我们提供两种主流的Dijkstra算法实现。

5.1 基于邻接矩阵的实现 ( O ( V 2 ) O(V^2) O(V2))

这种实现方式直观,易于理解,但效率较低,适用于稠密图(边数 E 接近 V 2 V^2 V2)。

5.1.1 复杂度分析

  • 时间复杂度: 外层有一个 V 次的循环。内层循环为了找到 dist 最小的未访问顶点,需要遍历所有 V 个顶点,这耗时 O ( V ) O(V) O(V)。松弛操作总共会对每条边操作一次,在邻接矩阵中,这部分嵌套在两层循环内。总的时间复杂度是 O ( V 2 ) O(V^2) O(V2)
  • 空间复杂度: 需要一个邻接矩阵存储图 O ( V 2 ) O(V^2) O(V2),以及 distvisited 数组 O ( V ) O(V) O(V)。总空间复杂度为 O ( V 2 ) O(V^2) O(V2)

5.1.2 C++ 代码实现

#include <iostream>
#include <vector>
#include <climits>

#define V 5 // 顶点数量
#define INF INT_MAX

// 寻找dist数组中未访问过的最小值的顶点索引
int minDistance(const std::vector<int>& dist, const std::vector<bool>& visited) {
    int min = INF, min_index = -1;
    for (int v = 0; v < V; ++v) {
        if (!visited[v] && dist[v] <= min) {
            min = dist[v];
            min_index = v;
        }
    }
    return min_index;
}

void dijkstra(int graph[V][V], int src) {
    std::vector<int> dist(V, INF);
    std::vector<bool> visited(V, false);

    dist[src] = 0; // 源点到自身距离为0

    // 对所有顶点进行操作
    for (int count = 0; count < V - 1; ++count) {
        // 1. 选点:选择当前dist最小的未访问顶点
        int u = minDistance(dist, visited);
        if (u == -1) break; // 所有可达顶点都已处理

        // 2. 标记:将选出的顶点标记为已访问
        visited[u] = true;

        // 3. 松弛:更新u的邻接点的dist值
        for (int v = 0; v < V; ++v) {
            // 条件:v未被访问,u-v有边,且通过u到v的路径更短
            if (!visited[v] && graph[u][v] && dist[u] != INF && dist[u] + graph[u][v] < dist[v]) {
                dist[v] = dist[u] + graph[u][v];
            }
        }
    }

    // 打印结果
    std::cout << "Vertex\tDistance from Source" << std::endl;
    for (int i = 0; i < V; ++i) {
        std::cout << i << "\t\t" << (dist[i] == INF ? "INF" : std::to_string(dist[i])) << std::endl;
    }
}

int main() {
    int graph[V][V] = {
        {0, 10, 3, 0, 0},
        {0, 0, 8, 1, 0},
        {0, 4, 0, 8, 2},
        {0, 0, 0, 0, 5},
        {0, 0, 0, 0, 0}
    };
    // 为了表示无穷,0表示无直连边,实际使用时需要区分0权重和无边
    // 这里的graph[u][v]=0代表无直连边

    dijkstra(graph, 0); // 从源点0开始
    return 0;
}

5.1.3 Java 代码实现

import java.util.Arrays;

public class DijkstraMatrix {
    private static final int V = 5;
    private static final int INF = Integer.MAX_VALUE;

    private int minDistance(int[] dist, boolean[] visited) {
        int min = INF;
        int minIndex = -1;
        for (int v = 0; v < V; v++) {
            if (!visited[v] && dist[v] <= min) {
                min = dist[v];
                minIndex = v;
            }
        }
        return minIndex;
    }

    public void dijkstra(int[][] graph, int src) {
        int[] dist = new int[V];
        boolean[] visited = new boolean[V];

        Arrays.fill(dist, INF);
        dist[src] = 0;

        for (int count = 0; count < V - 1; count++) {
            // 1. 选点
            int u = minDistance(dist, visited);
            if (u == -1) break;

            // 2. 标记
            visited[u] = true;

            // 3. 松弛
            for (int v = 0; v < V; v++) {
                if (!visited[v] && graph[u][v] != 0 && dist[u] != INF && dist[u] + graph[u][v] < dist[v]) {
                    dist[v] = dist[u] + graph[u][v];
                }
            }
        }

        printSolution(dist);
    }

    private void printSolution(int[] dist) {
        System.out.println("Vertex\tDistance from Source");
        for (int i = 0; i < V; i++) {
            System.out.println(i + "\t\t" + (dist[i] == INF ? "INF" : dist[i]));
        }
    }

    public static void main(String[] args) {
        int[][] graph = new int[][]{
            {0, 10, 3, 0, 0},
            {0, 0, 8, 1, 0},
            {0, 4, 0, 8, 2},
            {0, 0, 0, 0, 5},
            {0, 0, 0, 0, 0}
        };
        DijkstraMatrix t = new DijkstraMatrix();
        t.dijkstra(graph, 0);
    }
}

5.2 基于优先队列(堆)的优化 ( O ( E log ⁡ V ) O(E \log V) O(ElogV))

O ( V 2 ) O(V^2) O(V2) 的实现在“选点”这一步耗费了大量时间。如果图是稀疏的(边数 E 远小于 V 2 V^2 V2),这种实现就非常浪费。

5.2.1 优化的关键:为什么用优先队列?

“选点”的本质是在未访问的顶点中找到dist值最小的那个。这正是**优先队列(Min-Heap)**的专长!

我们可以将 (距离, 顶点) 二元组存入优先队列。

  • 选点:直接从优先队列顶部取元素,时间复杂度从 O ( V ) O(V) O(V) 降为 O ( log ⁡ V ) O(\log V) O(logV)
  • 松弛:当 dist[v] 被更新时,将新的、更小的 (dist[v], v) 放入优先队列。

5.2.2 复杂度分析

  • 时间复杂度:
    • 每个顶点入队和出队一次,共 V 次,每次操作 O ( log ⁡ V ) O(\log V) O(logV),总计 O ( V log ⁡ V ) O(V \log V) O(VlogV)
    • 每条边最多引起一次松弛操作(更新 dist 值),每次松弛会向优先队列中插入一个新元素,共 E 次,每次操作 O ( log ⁡ V ) O(\log V) O(logV),总计 O ( E log ⁡ V ) O(E \log V) O(ElogV)
    • 综合起来,总时间复杂度为 O ( V log ⁡ V + E log ⁡ V ) O(V \log V + E \log V) O(VlogV+ElogV)。对于连通图, E ≥ V − 1 E \ge V-1 EV1,所以可以简化为 O ( E log ⁡ V ) O(E \log V) O(ElogV)
  • 空间复杂度: 使用邻接表存储图,空间为 O ( V + E ) O(V+E) O(V+E)。优先队列最多存储 V 个元素,distvisited 数组需要 O ( V ) O(V) O(V)。总空间复杂度为 O ( V + E ) O(V+E) O(V+E)

5.2.3 C++ 代码实现

#include <iostream>
#include <vector>
#include <queue>
#include <climits>

using namespace std;

#define INF INT_MAX

// 定义图的邻接表表示
typedef pair<int, int> iPair; // (权重, 顶点)

void dijkstra_pq(int V, const vector<vector<iPair>>& adj, int src) {
    // 优先队列,存储<距离, 顶点>,默认为大顶堆,需转为小顶堆
    priority_queue<iPair, vector<iPair>, greater<iPair>> pq;
    
    vector<int> dist(V, INF);
    
    // 插入源点
    pq.push({0, src});
    dist[src] = 0;
    
    while (!pq.empty()) {
        // 1. 选点:自动选择dist最小的顶点u
        int u = pq.top().second;
        pq.pop();
        
        // 优化:如果一个顶点被处理过,则跳过
        // 因为队列中可能有旧的、距离更大的(dist, u)对
        if (dist[u] < pq.top().first && !pq.empty()) {
            continue;
        }

        // 2. 标记(隐式):从队列中取出即视为处理
        // 3. 松弛
        for (auto& edge : adj[u]) {
            int v = edge.second;
            int weight = edge.first;
            
            if (dist[u] != INF && dist[u] + weight < dist[v]) {
                dist[v] = dist[u] + weight;
                pq.push({dist[v], v}); // 将更新后的(距离, 顶点)放入队列
            }
        }
    }
    
    // 打印结果
    cout << "Vertex\tDistance from Source" << endl;
    for (int i = 0; i < V; ++i) {
        cout << i << "\t\t" << (dist[i] == INF ? "INF" : to_string(dist[i])) << endl;
    }
}

int main() {
    int V = 5;
    // 使用邻接表
    vector<vector<iPair>> adj(V);
    
    // 添加边,格式: adj[u].push_back({weight, v})
    adj[0].push_back({10, 1});
    adj[0].push_back({3, 2});
    adj[1].push_back({1, 3});
    adj[1].push_back({8, 2});
    adj[2].push_back({4, 1});
    adj[2].push_back({8, 3});
    adj[2].push_back({2, 4});
    adj[3].push_back({5, 4});

    dijkstra_pq(V, adj, 0);
    
    return 0;
}

5.2.4 Java 代码实现

import java.util.*;

public class DijkstraPriorityQueue {
    private static final int INF = Integer.MAX_VALUE;

    static class Node implements Comparable<Node> {
        public int vertex;
        public int distance;

        public Node(int vertex, int distance) {
            this.vertex = vertex;
            this.distance = distance;
        }

        @Override
        public int compareTo(Node other) {
            return Integer.compare(this.distance, other.distance);
        }
    }

    public void dijkstra(int V, List<List<Node>> adj, int src) {
        PriorityQueue<Node> pq = new PriorityQueue<>(V);
        int[] dist = new int[V];
        Arrays.fill(dist, INF);

        dist[src] = 0;
        pq.add(new Node(src, 0));

        while (!pq.isEmpty()) {
            // 1. 选点
            Node node = pq.poll();
            int u = node.vertex;

            // 优化
            if (node.distance > dist[u]) {
                continue;
            }

            // 3. 松弛
            for (Node neighbor : adj.get(u)) {
                int v = neighbor.vertex;
                int weight = neighbor.distance;

                if (dist[u] != INF && dist[u] + weight < dist[v]) {
                    dist[v] = dist[u] + weight;
                    pq.add(new Node(v, dist[v]));
                }
            }
        }
        
        printSolution(dist);
    }
    
    private void printSolution(int[] dist) {
        System.out.println("Vertex\tDistance from Source");
        for (int i = 0; i < dist.length; i++) {
            System.out.println(i + "\t\t" + (dist[i] == INF ? "INF" : dist[i]));
        }
    }

    public static void main(String[] args) {
        int V = 5;
        List<List<Node>> adj = new ArrayList<>();
        for (int i = 0; i < V; i++) {
            adj.add(new ArrayList<>());
        }
        
        adj.get(0).add(new Node(1, 10));
        adj.get(0).add(new Node(2, 3));
        adj.get(1).add(new Node(3, 1));
        adj.get(1).add(new Node(2, 8));
        adj.get(2).add(new Node(1, 4));
        adj.get(2).add(new Node(3, 8));
        adj.get(2).add(new Node(4, 2));
        adj.get(3).add(new Node(4, 5));

        DijkstraPriorityQueue solver = new DijkstraPriorityQueue();
        solver.dijkstra(V, adj, 0);
    }
}

六、Dijkstra 算法的局限性

Dijkstra 算法功能强大,但它有一个致命的弱点。

6.1 致命弱点:无法处理负权边

Dijkstra 算法的贪心策略成立的基础是:一旦一个顶点被标记为 visited,其 dist 值就不会再被更新。这是因为所有边权都是非负的,任何绕行路径的长度都不可能比当前已找到的路径更短。

但是,如果图中存在负权边,这个前提就被打破了。

反例:

3

5

-4

A

B

C

从 A 出发:

  1. 初始化: dist = {A:0, B:inf, C:inf}, visited = {}
  2. 第一次迭代: 选择 A。更新 dist = {A:0, B:3, C:5}
  3. 第二次迭代: 选择 B (dist=3)。将 B 加入 visited。更新 B 的邻居 C:dist[B] + w(B,C) = 3 + (-4) = -1 < dist[C]。所以 dist[C] 更新为 -1。此时 dist = {A:0, B:3, C:-1}
  4. 第三次迭代: 选择 C (dist=-1)。

最终结果 dist[C] = -1 是正确的。但是,如果图再复杂一点呢?

错误示例:

1

2

10

-10

A

B

C

D

从 A 出发:

  1. 第一次迭代: 选择 A,更新 dist={B:1, D:10}
  2. 第二次迭代: 选择 B (dist=1),将其标记为 visited。此时算法认定 A->B 的最短路就是1。然后更新 C,dist[C]=3
  3. 第三次迭代: 选择 C (dist=3)。
  4. 第四次迭代: 选择 D (dist=10)。更新 B: dist[D]+w(D,B) = 10-10=0 < dist[B]但是,B 已经被标记为 visited,Dijkstra 算法不会再去更新它!

算法最终会给出 A->B 的最短路是 1,但实际上 A->D->B 的路径长度是 0。Dijkstra的贪心选择过早地确定了到 B 的路径,导致了错误。

结论:当图中存在负权边时,必须使用其他算法,如 Bellman-Ford 算法SPFA 算法(将在后续文章中介绍)。

七、应用场景

Dijkstra 算法因其高效和直观,在没有负权边的场景下应用极为广泛:

  • 网络路由协议:如 OSPF (开放最短路径优先) 协议,在路由器之间交换链路状态信息,并使用 Dijkstra 算法计算路由表。
  • 地图导航:GPS 系统在计算两个地点间的最快或最短路线时,可以将交叉路口看作顶点,道路看作边,行驶时间或距离看作权重。
  • 社交网络:计算两个人之间的“最短关系链”。
  • 生物信息学:在基因调控网络中寻找最短的调控路径。

八、总结

本文系统地介绍了单源最短路径问题的经典解法——Dijkstra 算法。现在,让我们对核心知识点进行回顾:

  1. 核心思想:Dijkstra 是一种基于贪心策略的算法。它维护一个 dist 数组和一个 visited 集合,每次选择当前已知距离源点最近的未访问顶点,确定其最短路径,并用它来“松弛”其邻居的距离。
  2. 实现方式
    • 朴素实现(邻接矩阵):通过线性扫描查找 dist 最小的顶点,时间复杂度为 O ( V 2 ) O(V^2) O(V2),适用于稠密图。
    • 优化实现(优先队列+邻接表):利用优先队列(堆)快速获取 dist 最小的顶点,时间复杂度降至 O ( E log ⁡ V ) O(E \log V) O(ElogV),是稀疏图上的首选方案。
  3. 关键局限性:Dijkstra 算法的正确性依赖于边权的非负性。它无法正确处理带有负权边的图
  4. 下篇预告:既然 Dijkstra 无法处理负权边,那么当现实问题中出现“返利”、“消耗减少”等负权场景时,我们该如何应对呢?下一篇文章,我们将学习能够完美解决带负权边问题的 Bellman-Ford 算法

更多推荐