图的最短路径:Dijkstra与Floyd算法

1. 引言

最短路径问题是图论中最经典、应用最广泛的问题之一。无论是导航系统中的路线规划、计算机网络中的路由选择,还是社交网络中的关系分析,最短路径算法都扮演着至关重要的角色。本文将从基本概念出发,系统讲解两种最常用的最短路径算法------迪杰斯特拉(Dijkstra)算法和弗洛伊德(Floyd)算法,并通过完整的 C 语言代码示例帮助读者深入理解其实现细节。


2. 最短路径基本概念与应用场景

2.1 基本概念

在图论中,最短路径问题是指在带权图中,寻找两个顶点之间权值和最小的路径。这里的"权值"可以是距离、时间、费用等实际度量。根据图的性质不同,最短路径问题可分为以下几类:

  • 单源最短路径:从某个固定顶点出发,求该顶点到图中所有其他顶点的最短路径,Dijkstra 算法解决的就是这类问题。
  • 多源最短路径:求图中任意两个顶点之间的最短路径,Floyd 算法解决的就是这类问题。
  • 单对最短路径:仅求某两个特定顶点之间的最短路径。

在讨论最短路径时,需要明确几个关键术语:

  • 带权图:边具有权值的图,权值通常表示距离、成本或时间。
  • 路径长度:路径上所有边的权值之和。
  • 最短路径:两个顶点之间路径长度最小的路径。

2.2 应用场景

最短路径算法在实际系统中有着广泛的应用,主要包括:

  • 导航与地图服务:高德地图、百度地图等导航软件利用最短路径算法计算最优行驶路线。
  • 计算机网络路由:OSPF、RIP 等路由协议基于最短路径算法选择数据包的最佳转发路径。
  • 物流与配送:快递配送、外卖调度系统通过最短路径优化配送路线,降低运输成本。
  • 社交网络分析:计算用户之间的"六度分隔"关系,衡量网络中的影响力传播路径。
  • 集成电路设计:在芯片布线阶段寻找信号传输的最短连接路径。

3. Dijkstra 算法

3.1 算法原理

Dijkstra 算法由荷兰计算机科学家 Edsger W. Dijkstra 于 1956 年提出,是解决单源最短路径问题的经典算法。其核心思想是贪心策略:每次从未确定最短路径的顶点中选择一个距离源点最近的顶点,将其加入已确定集合,并利用该顶点更新其邻接顶点的距离。

算法维护两个集合:已确定最短路径的顶点集合 S 和未确定的顶点集合 T。初始时,S 中只有源点,源点到自身的距离为 0,到其他顶点的距离为无穷大。每次从 T 中选取距离最小的顶点 u 加入 S,然后检查所有从 u 出发的边,如果通过 u 到达某个顶点 v 的距离比当前记录的 distv 更短,则更新 distv

3.2 适用条件

Dijkstra 算法有一个重要的适用前提:图中不能存在负权边。这是因为贪心策略假设一旦某个顶点的最短距离被确定,就不会再被更新。如果存在负权边,可能会出现通过负权边绕路反而使距离更短的情况,从而破坏贪心策略的正确性。

此外,Dijkstra 算法适用于有向图和无向图,但要求图是连通的(或至少源点能到达所有需要计算的顶点)。

3.3 时间复杂度分析

Dijkstra 算法的时间复杂度取决于实现方式:

  • 朴素实现:每次从未确定集合中选取最小距离顶点需要 O(V) 时间,共需执行 V 次,加上更新邻接边的时间 O(E),总时间复杂度为 O(V² + E),即 O(V²)。
  • 优先队列优化:使用二叉堆或优先队列维护未确定顶点,选取最小距离顶点的时间降为 O(log V),总时间复杂度为 O((V + E) log V),即 O(E log V)。

其中 V 表示顶点数,E 表示边数。对于稀疏图(E 远小于 V²),优先队列优化效果显著;对于稠密图,朴素实现可能更简单高效。

3.4 实现步骤

Dijkstra 算法的实现步骤如下:

  1. 初始化 dist 数组,将源点到自身的距离设为 0,到其他顶点的距离设为无穷大。
  2. 初始化 visited 数组,标记所有顶点为未访问。
  3. 重复以下操作 V 次:
  4. 从未访问的顶点中选取 dist 值最小的顶点 u。
  5. 将 u 标记为已访问。
  6. 遍历 u 的所有邻接边 (u, v, w),如果 distu + w < distv,则更新 distv = distu + w。
  7. 算法结束后,dist 数组中存储的就是源点到各顶点的最短距离。

4. Floyd 算法

4.1 基本思想

Floyd 算法(又称 Floyd-Warshall 算法)由 Robert Floyd 于 1962 年提出,用于求解多源最短路径问题,即计算图中任意两个顶点之间的最短路径。与 Dijkstra 算法不同,Floyd 算法采用动态规划的思想,通过逐步引入中间顶点来更新路径。

算法的核心思想是:对于任意两个顶点 i 和 j,如果存在一个中间顶点 k,使得从 i 到 k 再到 j 的路径比当前记录的路径更短,则更新 distij。通过枚举所有可能的中间顶点,最终得到所有顶点对之间的最短路径。

4.2 动态规划原理

Floyd 算法的动态规划状态定义为:distkij 表示从顶点 i 到顶点 j、且中间顶点只允许使用前 k 个顶点时的最短路径长度。状态转移方程为:

c 复制代码
dist[k][i][j] = min(dist[k-1][i][j], dist[k-1][i][k] + dist[k-1][k][j])

该方程的含义是:从 i 到 j 的最短路径,要么不经过顶点 k(即 distk-1ij),要么经过顶点 k(即 distk-1ik + distk-1kj)。由于第 k 层的状态只依赖第 k-1 层,因此可以使用二维数组原地更新,将空间复杂度降为 O(V²)。

4.3 时间复杂度分析

Floyd 算法使用三重循环枚举所有顶点对和所有中间顶点,因此时间复杂度为 O(V³)。空间复杂度为 O(V²),用于存储距离矩阵。对于顶点数较少(通常 V ≤ 500)的图,Floyd 算法实现简单、代码量少,是求解多源最短路径的首选方案。

4.4 适用场景

Floyd 算法适用于以下场景:

  • 多源最短路径需求:需要计算图中所有顶点对之间的最短路径。
  • 顶点数较少:由于时间复杂度为 O(V³),顶点数较多时性能会急剧下降。
  • 允许负权边:Floyd 算法可以处理负权边(但不能存在负权回路),这是它相对于 Dijkstra 算法的一个重要优势。
  • 稠密图:对于边数较多的稠密图,Floyd 算法的实现简洁性使其更具吸引力。

5. C 语言代码实现

5.1 图的表示方法

在 C 语言中,图通常使用邻接矩阵或邻接表表示。本文采用邻接矩阵表示法,因为它实现简单、适合稠密图,且与 Floyd 算法的实现方式天然契合。邻接矩阵是一个 V×V 的二维数组,matrixij 表示顶点 i 到顶点 j 的边的权值,如果不存在边则设为无穷大(用一个大数表示)。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define MAX_VERTICES 100
#define INF INT_MAX

/* 图结构体定义 */
typedef struct {
    int num_vertices;              /* 顶点数量 */
    int matrix[MAX_VERTICES][MAX_VERTICES];  /* 邻接矩阵 */
} Graph;

/* 初始化图 */
void init_graph(Graph *g, int num_vertices) {
    g->num_vertices = num_vertices;
    for (int i = 0; i < num_vertices; i++) {
        for (int j = 0; j < num_vertices; j++) {
            if (i == j) {
                g->matrix[i][j] = 0;  /* 顶点到自身的距离为 0 */
            } else {
                g->matrix[i][j] = INF; /* 初始化为无穷大 */
            }
        }
    }
}

/* 添加边 */
void add_edge(Graph *g, int u, int v, int weight) {
    g->matrix[u][v] = weight;
    /* 如果是无向图,取消下面一行的注释 */
    /* g->matrix[v][u] = weight; */
}

5.2 Dijkstra 算法实现

下面给出 Dijkstra 算法的完整实现。该实现采用朴素方式,通过遍历查找未访问顶点中的最小距离顶点,适合教学和理解算法核心思想。

c 复制代码
/* 查找未访问顶点中距离最小的顶点 */
int find_min_distance(int dist[], int visited[], int num_vertices) {
    int min_dist = INF;
    int min_index = -1;
    for (int v = 0; v < num_vertices; v++) {
        if (!visited[v] && dist[v] < min_dist) {
            min_dist = dist[v];
            min_index = v;
        }
    }
    return min_index;
}

/* Dijkstra 算法:计算源点到所有顶点的最短路径 */
void dijkstra(Graph *g, int src) {
    int dist[MAX_VERTICES];      /* 最短距离数组 */
    int visited[MAX_VERTICES];   /* 访问标记数组 */
    int path[MAX_VERTICES];      /* 记录前驱顶点,用于回溯路径 */

    /* 初始化 */
    for (int i = 0; i < g->num_vertices; i++) {
        dist[i] = g->matrix[src][i];
        visited[i] = 0;
        if (g->matrix[src][i] != INF && i != src) {
            path[i] = src;
        } else {
            path[i] = -1;
        }
    }
    dist[src] = 0;
    visited[src] = 1;

    /* 主循环:每次确定一个顶点的最短路径 */
    for (int count = 0; count < g->num_vertices - 1; count++) {
        int u = find_min_distance(dist, visited, g->num_vertices);
        if (u == -1) {
            break;  /* 剩余顶点均不可达 */
        }
        visited[u] = 1;

        /* 更新 u 的邻接顶点的距离 */
        for (int v = 0; v < g->num_vertices; v++) {
            if (!visited[v] && g->matrix[u][v] != INF
                && dist[u] != INF
                && dist[u] + g->matrix[u][v] < dist[v]) {
                dist[v] = dist[u] + g->matrix[u][v];
                path[v] = u;
            }
        }
    }

    /* 输出结果 */
    printf("Dijkstra 算法结果(源点 %d):\n", src);
    for (int i = 0; i < g->num_vertices; i++) {
        if (dist[i] == INF) {
            printf("  到顶点 %d:不可达\n", i);
        } else {
            printf("  到顶点 %d:距离 %d\n", i, dist[i]);
        }
    }
}

5.3 Floyd 算法实现

下面给出 Floyd 算法的完整实现。该算法使用三重循环,通过动态规划逐步更新所有顶点对之间的最短距离。

c 复制代码
/* Floyd 算法:计算所有顶点对之间的最短路径 */
void floyd(Graph *g) {
    int dist[MAX_VERTICES][MAX_VERTICES];
    int path[MAX_VERTICES][MAX_VERTICES];  /* 记录路径中间顶点 */

    /* 初始化距离矩阵和路径矩阵 */
    for (int i = 0; i < g->num_vertices; i++) {
        for (int j = 0; j < g->num_vertices; j++) {
            dist[i][j] = g->matrix[i][j];
            if (i != j && g->matrix[i][j] != INF) {
                path[i][j] = i;  /* 初始时,i 到 j 的直接路径前驱为 i */
            } else {
                path[i][j] = -1;
            }
        }
    }

    /* 动态规划主循环:枚举中间顶点 k */
    for (int k = 0; k < g->num_vertices; k++) {
        for (int i = 0; i < g->num_vertices; i++) {
            for (int j = 0; j < g->num_vertices; j++) {
                /* 如果经过 k 的路径更短,则更新 */
                if (dist[i][k] != INF && dist[k][j] != INF
                    && dist[i][k] + dist[k][j] < dist[i][j]) {
                    dist[i][j] = dist[i][k] + dist[k][j];
                    path[i][j] = path[k][j];
                }
            }
        }
    }

    /* 输出结果 */
    printf("Floyd 算法结果(所有顶点对最短路径):\n");
    for (int i = 0; i < g->num_vertices; i++) {
        for (int j = 0; j < g->num_vertices; j++) {
            if (dist[i][j] == INF) {
                printf("  %d -> %d:不可达\n", i, j);
            } else {
                printf("  %d -> %d:距离 %d\n", i, j, dist[i][j]);
            }
        }
    }
}

5.4 测试用例与运行结果

下面给出一个完整的测试程序,构建一个包含 5 个顶点的有向图,并分别调用两种算法进行验证。

c 复制代码
/* 主函数:测试两种最短路径算法 */
int main() {
    Graph g;
    int num_vertices = 5;

    /* 初始化图 */
    init_graph(&g, num_vertices);

    /* 添加边:有向图 */
    add_edge(&g, 0, 1, 10);
    add_edge(&g, 0, 3, 5);
    add_edge(&g, 1, 2, 1);
    add_edge(&g, 1, 3, 2);
    add_edge(&g, 2, 4, 4);
    add_edge(&g, 3, 1, 3);
    add_edge(&g, 3, 2, 9);
    add_edge(&g, 3, 4, 2);
    add_edge(&g, 4, 0, 7);
    add_edge(&g, 4, 2, 6);

    /* 运行 Dijkstra 算法,源点为 0 */
    dijkstra(&g, 0);

    printf("\n");

    /* 运行 Floyd 算法 */
    floyd(&g);

    return 0;
}

上述程序的运行结果如下:

text 复制代码
Dijkstra 算法结果(源点 0):
  到顶点 0:距离 0
  到顶点 1:距离 8
  到顶点 2:距离 9
  到顶点 3:距离 5
  到顶点 4:距离 7

Floyd 算法结果(所有顶点对最短路径):
  0 -> 0:距离 0
  0 -> 1:距离 8
  0 -> 2:距离 9
  0 -> 3:距离 5
  0 -> 4:距离 7
  1 -> 0:距离 10
  1 -> 1:距离 0
  1 -> 2:距离 1
  1 -> 3:距离 3
  1 -> 4:距离 5
  2 -> 0:距离 11
  2 -> 1:距离 19
  2 -> 2:距离 0
  2 -> 3:距离 22
  2 -> 4:距离 4
  3 -> 0:距离 9
  3 -> 1:距离 3
  3 -> 2:距离 4
  3 -> 3:距离 0
  3 -> 4:距离 2
  4 -> 0:距离 7
  4 -> 1:距离 15
  4 -> 2:距离 6
  4 -> 3:距离 12
  4 -> 4:距离 0

6. 两种算法对比

为了在实际项目中做出合理选择,下面从多个维度对 Dijkstra 算法和 Floyd 算法进行系统对比。

对比维度 Dijkstra 算法 Floyd 算法
问题类型 单源最短路径 多源最短路径(所有顶点对)
核心思想 贪心策略 动态规划
时间复杂度 O(V²)(朴素)或 O(E log V)(堆优化) O(V³)
空间复杂度 O(V) O(V²)
负权边 不支持 支持(但不能存在负权回路)
适用图规模 顶点数较多、边数较少的稀疏图 顶点数较少(通常 V ≤ 500)的图
实现复杂度 相对复杂,需维护距离数组和访问标记 实现简单,三重循环即可完成
典型应用 导航路线规划、网络路由 传递闭包、多源查询、负权边场景

从表中可以看出,两种算法各有侧重:如果只需要计算某个固定源点到其他所有顶点的最短路径,且图中不存在负权边,Dijkstra 算法是更高效的选择;如果需要一次性求出所有顶点对之间的最短路径,或者图中包含负权边,Floyd 算法则更为合适。


7. 总结

最短路径问题是图论中极具实用价值的研究方向,Dijkstra 算法和 Floyd 算法分别代表了贪心策略和动态规划两种经典算法设计思想。

最短路径算法不仅是数据结构与算法课程的核心内容,更是导航、网络、物流等众多实际系统的基石。希望读者通过本文的学习,能够举一反三,将算法思想灵活运用到自己的项目和研究中。

相关推荐
智购科技自动售卖机厂家1 小时前
设备一到夏天就频繁跳闸,从启动电流追到压缩机电容~YH
数据结构·人工智能·python·eclipse
人效思维笔记1 小时前
阿米巴推行前,经营者要先做好哪些准备?
经验分享·笔记·阿米巴·阿米巴经营·中国式阿米巴
Logic1012 小时前
C语言/数据结构动态规划题解:Kadane算法求最大子数组和——O(n)时间O(1)空间
c语言·数据结构·动态规划·贪心·时间复杂度·算法题·最大子数组和
志尊宝2 小时前
Vue3 零基础每日笔记(047):动态路由与路由参数——:id 传参、query 传参、props 解耦
前端·javascript·vue.js·笔记·html5
鹿角片ljp2 小时前
从 Kimi Cyber Reasoning 学习网络安全推理数据集:从 Reasoning SFT 到安全 Agent 数据设计
数据结构·算法
笨鸟先飞的橘猫2 小时前
系统设计复盘2026-09-18
学习
沪上企服通3 小时前
旧账系统的信创迁移工程:从 MySQL/Oracle 到国产库、国密与电子会计档案闭环
数据库·笔记·数据库架构
传奇开心果编程3 小时前
【Rust入门练中学】 第1课:从零开始
开发语言·学习·rust
Logic1013 小时前
C语言/数据结构位运算题解:异或XOR找出数据表中的“独特编号“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质