图的存储结构详解:邻接矩阵的原理、实现与应用

1. 引言

图(Graph)是计算机科学中一种非常重要的非线性数据结构,它由顶点(Vertex)和连接顶点的边(Edge)组成。无论是社交网络中的好友关系、地图导航中的道路网络,还是计算机网络中的路由拓扑,都可以抽象为图来进行建模和分析。而如何高效地存储和表示图,直接决定了相关算法的性能与实现的复杂度。本文将围绕图的存储结构展开,重点深入剖析邻接矩阵(Adjacency Matrix)这一经典存储方式,从定义、数学表示、代码实现、复杂度分析到实际应用场景,为读者提供一份系统而实用的技术参考。


2. 图的基本概念与分类

在深入讨论存储结构之前,有必要先厘清图的基本概念。图 G 可以形式化地定义为一个二元组 G = (V, E),其中 V 是顶点的有限集合,E 是边的有限集合。每条边连接两个顶点,表示它们之间存在某种关系。

根据边的特征,图可以划分为以下几种常见类型:

  • 无向图(Undirected Graph):边没有方向,顶点 u 和 v 之间的边 (u, v) 与 (v, u) 表示同一条边,例如朋友关系。
  • 有向图(Directed Graph):边具有方向,顶点 u 指向 v 的边记为 <u, v>,与 <v, u> 是两条不同的边,例如网页之间的超链接关系。
  • 带权图(Weighted Graph):边带有权值,权值可以表示距离、成本、时间等度量,例如城市之间的公路里程。
  • 稀疏图与稠密图:根据边数相对于顶点数的比例,边数较少的图称为稀疏图,边数接近顶点数平方的图称为稠密图。这一分类对存储结构的选择至关重要。

此外,还有完全图(任意两个顶点之间都有边)、连通图(任意两个顶点之间都存在路径)等概念。理解这些分类,有助于我们针对不同场景选择最合适的存储方式。


3. 邻接矩阵的定义与数学表示

邻接矩阵是图的一种顺序存储结构,它使用一个二维数组来记录图中任意两个顶点之间是否存在边(以及边的权值)。

假设图 G 包含 n 个顶点,则邻接矩阵是一个 n × n 的方阵 A,其中元素 Aij 的定义如下:

  • 对于无权图:Aij = 1 表示顶点 i 到顶点 j 之间存在边;Aij = 0 表示不存在边。
  • 对于带权图:Aij = w 表示顶点 i 到顶点 j 之间的边权值为 w;当不存在边时,通常用无穷大(∞)或一个特殊值(如 0 或 -1)表示。

数学上,邻接矩阵可以简洁地表示为:

text 复制代码
A[i][j] = 1(或权值 w),若存在从顶点 i 到顶点 j 的边;
A[i][j] = 0(或 ∞),若不存在从顶点 i 到顶点 j 的边。

对于无向图,由于边 (u, v) 与 (v, u) 等价,其邻接矩阵必然是一个对称矩阵,即 Aij = Aji。这一对称性既是无向图邻接矩阵的重要特征,也可以用于存储优化(例如只存储上三角或下三角部分)。

下面以一个包含 4 个顶点的无向图为例进行说明。假设顶点集合为 V = {0, 1, 2, 3},边集合为 E = {(0,1), (0,2), (1,2), (2,3)},则其邻接矩阵为:

text 复制代码
    0  1  2  3
0   0  1  1  0
1   1  0  1  0
2   1  1  0  1
3   0  0  1  0

可以看到,矩阵关于主对角线对称,且对角线元素通常为 0(表示顶点自身没有边)。


4. 邻接矩阵的具体实现方法

邻接矩阵的实现思路非常直观:使用一个二维数组存储顶点之间的关系,同时使用一个一维数组(或列表)存储顶点本身的信息。下面分别针对无向图、有向图和带权图给出具体的实现方式。

4.1 无向图的邻接矩阵实现

无向图的邻接矩阵是对称的,插入一条边时需要同时更新 Aij 和 Aji。以下是使用 C 语言的实现示例:

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

#define MAX_VERTICES 100

typedef struct {
    int n;                      // 顶点数
    int matrix[MAX_VERTICES][MAX_VERTICES]; // 邻接矩阵
} UndirectedGraph;

// 初始化图
void initGraph(UndirectedGraph *g, int n) {
    g->n = n;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            g->matrix[i][j] = 0;
        }
    }
}

// 添加边(无向图需对称更新)
void addEdge(UndirectedGraph *g, int u, int v) {
    if (u >= 0 && u < g->n && v >= 0 && v < g->n) {
        g->matrix[u][v] = 1;
        g->matrix[v][u] = 1;
    }
}

// 判断两个顶点之间是否存在边
bool hasEdge(UndirectedGraph *g, int u, int v) {
    return g->matrix[u][v] == 1;
}

// 打印邻接矩阵
void printGraph(UndirectedGraph *g) {
    for (int i = 0; i < g->n; i++) {
        for (int j = 0; j < g->n; j++) {
            printf("%d ", g->matrix[i][j]);
        }
        printf("\n");
    }
}

int main() {
    UndirectedGraph g;
    initGraph(&g, 4);
    addEdge(&g, 0, 1);
    addEdge(&g, 0, 2);
    addEdge(&g, 1, 2);
    addEdge(&g, 2, 3);
    printGraph(&g);
    return 0;
}

4.2 有向图的邻接矩阵实现

有向图的邻接矩阵不再对称,插入边 <u, v> 时只需更新 Auv。以下是使用 C 语言的实现示例:

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

#define MAX_VERTICES 100

typedef struct {
    int n;                      // 顶点数
    int matrix[MAX_VERTICES][MAX_VERTICES]; // 邻接矩阵
} DirectedGraph;

// 初始化图
void initGraph(DirectedGraph *g, int n) {
    g->n = n;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            g->matrix[i][j] = 0;
        }
    }
}

// 添加有向边 u -> v
void addEdge(DirectedGraph *g, int u, int v) {
    if (u >= 0 && u < g->n && v >= 0 && v < g->n) {
        g->matrix[u][v] = 1;
    }
}

// 判断是否存在 u -> v 的边
bool hasEdge(DirectedGraph *g, int u, int v) {
    return g->matrix[u][v] == 1;
}

// 打印邻接矩阵
void printGraph(DirectedGraph *g) {
    for (int i = 0; i < g->n; i++) {
        for (int j = 0; j < g->n; j++) {
            printf("%d ", g->matrix[i][j]);
        }
        printf("\n");
    }
}

int main() {
    DirectedGraph g;
    initGraph(&g, 4);
    addEdge(&g, 0, 1);
    addEdge(&g, 0, 2);
    addEdge(&g, 1, 2);
    addEdge(&g, 2, 3);
    printGraph(&g);
    return 0;
}

4.3 带权图的邻接矩阵实现

带权图的邻接矩阵中,矩阵元素存储的是边的权值。当两个顶点之间不存在边时,通常使用一个足够大的数(如 INT_MAX)表示无穷大。以下是使用 C 语言的实现示例:

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

#define MAX_VERTICES 100

typedef struct {
    int n;                      // 顶点数
    int matrix[MAX_VERTICES][MAX_VERTICES]; // 邻接矩阵
} WeightedGraph;

// 初始化图,对角线为 0,其余为无穷大
void initGraph(WeightedGraph *g, int n) {
    g->n = n;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            if (i == j) {
                g->matrix[i][j] = 0;
            } else {
                g->matrix[i][j] = INT_MAX;
            }
        }
    }
}

// 添加带权边(无向图示例,对称更新)
void addEdge(WeightedGraph *g, int u, int v, int weight) {
    if (u >= 0 && u < g->n && v >= 0 && v < g->n) {
        g->matrix[u][v] = weight;
        g->matrix[v][u] = weight;
    }
}

// 获取两个顶点之间的边权值
int getWeight(WeightedGraph *g, int u, int v) {
    return g->matrix[u][v];
}

// 打印邻接矩阵
void printGraph(WeightedGraph *g) {
    for (int i = 0; i < g->n; i++) {
        for (int j = 0; j < g->n; j++) {
            if (g->matrix[i][j] == INT_MAX) {
                printf("INF ");
            } else {
                printf("%d ", g->matrix[i][j]);
            }
        }
        printf("\n");
    }
}

int main() {
    // 示例:4 个顶点的带权无向图
    WeightedGraph g;
    initGraph(&g, 4);
    addEdge(&g, 0, 1, 5);
    addEdge(&g, 0, 2, 3);
    addEdge(&g, 1, 2, 2);
    addEdge(&g, 2, 3, 7);
    printGraph(&g);
    return 0;
}

在上述实现中,对角线元素设为 0,表示顶点到自身的距离为 0;不存在的边用无穷大表示,这样在后续执行最短路径等算法时可以直接使用矩阵进行运算。


5. 邻接矩阵的时间复杂度与空间复杂度分析

邻接矩阵的复杂度分析是理解其适用场景的关键。下面从时间复杂度和空间复杂度两个维度进行剖析。

5.1 时间复杂度

  • 判断两个顶点之间是否存在边:直接访问 Aij,时间复杂度为 O(1)。这是邻接矩阵最大的优势。
  • 添加或删除一条边:直接修改 Aij(无向图还需修改 Aji),时间复杂度为 O(1)。
  • 遍历某个顶点的所有邻接顶点:需要扫描矩阵中对应的一整行,时间复杂度为 O(n)。
  • 遍历整张图的所有边:需要扫描整个矩阵,时间复杂度为 O(n²)。

5.2 空间复杂度

邻接矩阵需要存储 n × n 个元素,因此空间复杂度为 O(n²)。无论图是稀疏还是稠密,这一空间开销都是固定的。对于顶点数 n 较大的稀疏图(例如 n = 10000 而边数只有 20000),邻接矩阵将浪费大量空间,此时邻接表等链式存储结构更具优势。

下表总结了邻接矩阵在不同操作上的复杂度表现:

操作 时间复杂度 说明
判断边是否存在 O(1) 直接访问矩阵元素
添加 / 删除边 O(1) 直接修改矩阵元素
遍历某顶点的邻接点 O(n) 扫描对应行
遍历所有边 O(n²) 扫描整个矩阵
空间复杂度 O(n²) 固定开销,与边数无关

6. 邻接矩阵的优缺点对比

任何存储结构都有其适用边界,邻接矩阵也不例外。下面从优点和缺点两个角度进行系统对比。

6.1 优点

  • 实现简单直观:二维数组的语义与图的数学定义高度一致,代码实现和理解成本低。
  • 判断边是否存在非常高效:O(1) 的时间复杂度使得频繁查询边关系的场景(如 Floyd 最短路径算法)表现优异。
  • 便于矩阵运算:邻接矩阵天然支持线性代数运算,例如计算图的传递闭包、统计长度为 k 的路径数量等,可以直接借助矩阵乘法实现。
  • 适合稠密图:当边数接近 n² 时,邻接矩阵的空间利用率高,且没有链表带来的指针开销。

6.2 缺点

  • 空间浪费严重:对于稀疏图,大量矩阵元素为 0 或无穷大,存储利用率低。
  • 扩展性差:顶点数 n 在初始化时即固定,动态增加顶点需要重建整个矩阵,成本较高。
  • 遍历邻接点效率低:即使某个顶点的邻接点很少,也需要扫描整行,时间复杂度为 O(n)。
  • 无向图存在冗余存储:对称矩阵导致一半空间被重复存储(可通过压缩存储优化,但会增加实现复杂度)。

7. 邻接矩阵的实际应用场景

尽管邻接矩阵存在空间开销大的缺点,但在许多实际场景中,它依然是首选甚至唯一可行的存储方案。

7.1 图论算法中的矩阵运算

Floyd-Warshall 算法(多源最短路径)是邻接矩阵最经典的应用之一。该算法需要频繁查询和更新任意两个顶点之间的最短距离,邻接矩阵 O(1) 的随机访问能力使其成为天然适配的数据结构。此外,计算图的传递闭包(Warshall 算法)、统计路径数量等操作也依赖矩阵乘法,邻接矩阵可以直接参与运算。

7.2 稠密图场景

当图的边数接近顶点数的平方时(如完全图、竞赛图),邻接矩阵的空间利用率接近 100%,此时它比邻接表更节省内存(邻接表需要额外的指针存储)。例如在社交网络分析中,小规模群体内的强连接关系图往往接近稠密图,使用邻接矩阵可以获得更好的性能。

7.3 小规模图与教学演示

对于顶点数较少(如 n ≤ 100)的图,邻接矩阵的空间开销完全可以接受,且实现简单、易于调试。在算法教学、竞赛入门和原型验证阶段,邻接矩阵是最常用的选择。

7.4 图神经网络中的邻接矩阵

在深度学习领域,图神经网络(GNN)通常以邻接矩阵作为图的标准化输入表示。虽然大规模图会使用稀疏矩阵存储,但邻接矩阵的数学形式(配合度矩阵、拉普拉斯矩阵)是图卷积等操作的理论基础。例如,图卷积网络(GCN)的层间传播公式就建立在邻接矩阵的归一化运算之上。


8. 邻接矩阵与邻接表的适用场景比较

邻接表(Adjacency List)是另一种主流的图存储结构,它使用一个数组存储所有顶点,每个顶点对应一个链表(或动态数组),链表中存储该顶点的所有邻接顶点。下面从多个维度对两种结构进行对比。

对比维度 邻接矩阵 邻接表
空间复杂度 O(n²),固定开销 O(n + e),与边数成正比
判断边是否存在 O(1),直接访问 O(degree),需遍历链表
遍历某顶点的邻接点 O(n),扫描整行 O(degree),只遍历该顶点的链表
添加 / 删除边 O(1) O(degree)(需查找链表)
适合图类型 稠密图 稀疏图
实现复杂度 简单 相对复杂
典型算法 Floyd、传递闭包 DFS、BFS、Dijkstra、Kruskal

从对比中可以看出,两种结构各有千秋,选择的关键在于图的稀疏程度和核心操作类型:

  • 如果图是稠密图 ,或者算法需要频繁判断任意两个顶点之间是否存在边(如 Floyd 算法),应优先选择邻接矩阵。
  • 如果图是稀疏图 ,或者算法需要频繁遍历某个顶点的所有邻接点(如 DFS、BFS、Dijkstra),应优先选择邻接表。
  • 如果图的规模较小(如 n ≤ 100),两种结构均可,但邻接矩阵实现更简单、调试更方便。
  • 如果图需要动态增删顶点,邻接表更灵活;邻接矩阵在顶点数变化时需要重建整个矩阵,成本较高。

9. 总结

本文系统介绍了图的存储结构,并重点剖析了邻接矩阵这一经典方案。我们从图的基本概念与分类出发,明确了无向图、有向图、带权图以及稀疏图与稠密图的区别;随后给出了邻接矩阵的定义与数学表示,并通过具体示例展示了其对称性等特征。

在实现层面,我们分别使用 C++、Java 和 Python 演示了无向图、有向图与带权图的邻接矩阵实现方式。复杂度分析表明,邻接矩阵在判断边是否存在、添加或删除边等操作上具有 O(1) 的优异性能,但空间开销固定为 O(n²),更适合稠密图场景。

通过优缺点对比和实际应用场景分析可以看出,邻接矩阵在 Floyd 算法、传递闭包计算、图神经网络等场景中具有不可替代的优势;而邻接表则在稀疏图、频繁遍历邻接点等场景中表现更佳。实际开发中,应根据图的规模、稀疏程度以及核心操作类型,权衡选择最合适的存储结构。

希望本文能帮助读者深入理解邻接矩阵的原理与实现,并在实际项目中做出合理的技术选型。

相关推荐
平头哥AI1 小时前
Day 22 _ 包装错误别丢链_%w、errors.Is 与 errors.As
android·服务器·学习·golang·go
嵌入式学习菌1 小时前
Modbus‑RTU 数据类型分析
开发语言·单片机·bug
一阵寒风1 小时前
CAM智能化助力PCB 智能制造-培训体系第六章.项目开发学习
学习·制造
传奇开心果编程2 小时前
【Xilem 0.4 基础语法学与练】第15课:状态管理与 memoize 性能优化
学习·rust·前端框架
具身AGI2 小时前
视频即仿真,物理AI 人类学习路线 的下一步
人工智能·学习
matlab代码2 小时前
基于matlab数字图像处理的指纹识别系统【源码68期】
开发语言·matlab
matlab代码2 小时前
基于matlab的水果识别系统(苹果香蕉菠萝梨桃子)【源码65期】
开发语言·matlab
wuyk5553 小时前
从零吃透 MQTT 通信|第 8 章 FreeRTOS 多任务架构下 MQTT 工程架构,任务拆分、队列解耦、临界区保护
c语言·开发语言·stm32·学习·架构
weixin199701080163 小时前
[特殊字符]《跨境二手ERP对接Back Market:标准化API + 7~10工作日技术合规审核实录》(附Python源码)
开发语言·python·pandas