【数据结构与算法】第37篇:图论(一):图的存储结构(邻接矩阵与邻接表)

一、图的基本概念

1.1 图的定义

图由顶点集合和边集合组成:G = (V, E)

  • 有向图:边有方向(如微博关注)

  • 无向图:边无方向(如好友关系)

1.2 基本术语

术语 说明
顶点/节点 图中的基本元素
边/弧 顶点之间的连接
顶点的边数(有向图分入度和出度)
邻接 两个顶点之间有边相连
路径 从一个顶点到另一个顶点的顶点序列

二、邻接矩阵(顺序存储)

2.1 原理

用二维数组 matrix[i][j] 表示顶点 i 和 j 的连接关系:

  • 无权图:1表示有边,0表示无边

  • 有权图:存储权值,通常用无穷大表示无边

text

复制代码
无向图:
    A — B
    |   |
    C — D

邻接矩阵:
    A B C D
A   0 1 1 0
B   1 0 0 1
C   1 0 0 1
D   0 1 1 0

2.2 代码实现

c

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

#define MAX_VERTICES 100
#define INF 999999

typedef struct {
    int matrix[MAX_VERTICES][MAX_VERTICES];
    int vertexCount;
    int isDirected;  // 1:有向图, 0:无向图
} GraphMatrix;

// 初始化
void initMatrixGraph(GraphMatrix *g, int n, int directed) {
    g->vertexCount = n;
    g->isDirected = directed;
    
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            g->matrix[i][j] = (i == j) ? 0 : INF;
        }
    }
}

// 添加边
void addEdgeMatrix(GraphMatrix *g, int u, int v, int weight) {
    if (u < 0 || u >= g->vertexCount || v < 0 || v >= g->vertexCount) return;
    
    g->matrix[u][v] = weight;
    if (!g->isDirected) {
        g->matrix[v][u] = weight;
    }
}

// 删除边
void removeEdgeMatrix(GraphMatrix *g, int u, int v) {
    if (u < 0 || u >= g->vertexCount || v < 0 || v >= g->vertexCount) return;
    
    g->matrix[u][v] = INF;
    if (!g->isDirected) {
        g->matrix[v][u] = INF;
    }
}

// 判断是否有边
int hasEdge(GraphMatrix *g, int u, int v) {
    return g->matrix[u][v] != INF;
}

// 打印邻接矩阵
void printMatrixGraph(GraphMatrix *g) {
    printf("邻接矩阵:\n   ");
    for (int i = 0; i < g->vertexCount; i++) {
        printf("%4c", 'A' + i);
    }
    printf("\n");
    
    for (int i = 0; i < g->vertexCount; i++) {
        printf("%c  ", 'A' + i);
        for (int j = 0; j < g->vertexCount; j++) {
            if (g->matrix[i][j] == INF) {
                printf("  ∞");
            } else {
                printf("%4d", g->matrix[i][j]);
            }
        }
        printf("\n");
    }
}

int main() {
    GraphMatrix g;
    initMatrixGraph(&g, 4, 0);  // 无向图
    
    addEdgeMatrix(&g, 0, 1, 1);
    addEdgeMatrix(&g, 0, 2, 1);
    addEdgeMatrix(&g, 1, 3, 1);
    addEdgeMatrix(&g, 2, 3, 1);
    
    printMatrixGraph(&g);
    
    return 0;
}

运行结果:

text

复制代码
邻接矩阵:
    A   B   C   D
A    0   1   1   ∞
B    1   0   ∞   1
C    1   ∞   0   1
D    ∞   1   1   0

三、邻接表(链式存储)

3.1 原理

每个顶点维护一个链表,存储与之邻接的顶点。

text

复制代码
无向图:
    A — B
    |   |
    C — D

邻接表:
A → B → C → NULL
B → A → D → NULL
C → A → D → NULL
D → B → C → NULL

3.2 代码实现

c

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

// 邻接表节点
typedef struct EdgeNode {
    int vertex;              // 邻接顶点下标
    int weight;              // 权值
    struct EdgeNode *next;
} EdgeNode;

// 顶点节点
typedef struct {
    EdgeNode *first;         // 第一条边
} VertexNode;

// 图结构
typedef struct {
    VertexNode *vertices;    // 顶点数组
    int vertexCount;
    int edgeCount;
    int isDirected;
} GraphList;

// 初始化
void initListGraph(GraphList *g, int n, int directed) {
    g->vertexCount = n;
    g->edgeCount = 0;
    g->isDirected = directed;
    g->vertices = (VertexNode*)malloc(n * sizeof(VertexNode));
    
    for (int i = 0; i < n; i++) {
        g->vertices[i].first = NULL;
    }
}

// 添加边
void addEdgeList(GraphList *g, int u, int v, int weight) {
    if (u < 0 || u >= g->vertexCount || v < 0 || v >= g->vertexCount) return;
    
    // 添加 u -> v
    EdgeNode *newNode = (EdgeNode*)malloc(sizeof(EdgeNode));
    newNode->vertex = v;
    newNode->weight = weight;
    newNode->next = g->vertices[u].first;
    g->vertices[u].first = newNode;
    
    // 无向图添加 v -> u
    if (!g->isDirected) {
        EdgeNode *newNode2 = (EdgeNode*)malloc(sizeof(EdgeNode));
        newNode2->vertex = u;
        newNode2->weight = weight;
        newNode2->next = g->vertices[v].first;
        g->vertices[v].first = newNode2;
    }
    
    g->edgeCount++;
}

// 判断是否有边
int hasEdgeList(GraphList *g, int u, int v) {
    EdgeNode *p = g->vertices[u].first;
    while (p != NULL) {
        if (p->vertex == v) return 1;
        p = p->next;
    }
    return 0;
}

// 打印邻接表
void printListGraph(GraphList *g) {
    printf("邻接表:\n");
    for (int i = 0; i < g->vertexCount; i++) {
        printf("%c -> ", 'A' + i);
        EdgeNode *p = g->vertices[i].first;
        while (p != NULL) {
            printf("%c(%d) ", 'A' + p->vertex, p->weight);
            if (p->next) printf("-> ");
            p = p->next;
        }
        printf("NULL\n");
    }
}

// 销毁图
void destroyGraphList(GraphList *g) {
    for (int i = 0; i < g->vertexCount; i++) {
        EdgeNode *p = g->vertices[i].first;
        while (p != NULL) {
            EdgeNode *temp = p;
            p = p->next;
            free(temp);
        }
    }
    free(g->vertices);
}

int main() {
    GraphList g;
    initListGraph(&g, 4, 0);
    
    addEdgeList(&g, 0, 1, 1);
    addEdgeList(&g, 0, 2, 1);
    addEdgeList(&g, 1, 3, 1);
    addEdgeList(&g, 2, 3, 1);
    
    printListGraph(&g);
    
    destroyGraphList(&g);
    return 0;
}

运行结果:

text

复制代码
邻接表:
A -> C(1) -> B(1) NULL
B -> D(1) -> A(1) NULL
C -> D(1) -> A(1) NULL
D -> C(1) -> B(1) NULL

四、邻接矩阵 vs 邻接表

对比项 邻接矩阵 邻接表
空间复杂度 O(V²) O(V+E)
判断 u-v 是否有边 O(1) O(degree)
遍历某顶点的所有邻边 O(V) O(degree)
添加边 O(1) O(1)
删除边 O(1) O(degree)
实现复杂度 简单 中等
适用场景 稠密图 稀疏图

V = 顶点数,E = 边数,degree = 顶点的度


五、完整示例:同时演示两种存储

c

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

// 邻接矩阵图结构(同上)
typedef struct {
    int **matrix;
    int vertexCount;
    int isDirected;
} GraphMatrix;

// 邻接表图结构(同上)
typedef struct EdgeNode {
    int vertex;
    int weight;
    struct EdgeNode *next;
} EdgeNode;

typedef struct {
    EdgeNode **heads;
    int vertexCount;
    int isDirected;
} GraphList;

// 创建邻接矩阵
GraphMatrix* createMatrixGraph(int n, int directed) {
    GraphMatrix *g = (GraphMatrix*)malloc(sizeof(GraphMatrix));
    g->vertexCount = n;
    g->isDirected = directed;
    g->matrix = (int**)malloc(n * sizeof(int*));
    
    for (int i = 0; i < n; i++) {
        g->matrix[i] = (int*)malloc(n * sizeof(int));
        for (int j = 0; j < n; j++) {
            g->matrix[i][j] = (i == j) ? 0 : 999999;
        }
    }
    return g;
}

void addEdgeMatrix(GraphMatrix *g, int u, int v, int w) {
    g->matrix[u][v] = w;
    if (!g->isDirected) g->matrix[v][u] = w;
}

void printMatrixGraph(GraphMatrix *g) {
    printf("邻接矩阵:\n  ");
    for (int i = 0; i < g->vertexCount; i++) printf(" %2c", 'A'+i);
    printf("\n");
    for (int i = 0; i < g->vertexCount; i++) {
        printf("%c ", 'A'+i);
        for (int j = 0; j < g->vertexCount; j++) {
            if (g->matrix[i][j] == 999999) printf(" ∞");
            else printf("%2d", g->matrix[i][j]);
        }
        printf("\n");
    }
}

// 创建邻接表
GraphList* createListGraph(int n, int directed) {
    GraphList *g = (GraphList*)malloc(sizeof(GraphList));
    g->vertexCount = n;
    g->isDirected = directed;
    g->heads = (EdgeNode**)calloc(n, sizeof(EdgeNode*));
    return g;
}

void addEdgeList(GraphList *g, int u, int v, int w) {
    EdgeNode *e = (EdgeNode*)malloc(sizeof(EdgeNode));
    e->vertex = v;
    e->weight = w;
    e->next = g->heads[u];
    g->heads[u] = e;
    
    if (!g->isDirected) {
        EdgeNode *e2 = (EdgeNode*)malloc(sizeof(EdgeNode));
        e2->vertex = u;
        e2->weight = w;
        e2->next = g->heads[v];
        g->heads[v] = e2;
    }
}

void printListGraph(GraphList *g) {
    printf("\n邻接表:\n");
    for (int i = 0; i < g->vertexCount; i++) {
        printf("%c -> ", 'A'+i);
        EdgeNode *p = g->heads[i];
        while (p) {
            printf("%c(%d) ", 'A'+p->vertex, p->weight);
            if (p->next) printf("-> ");
            p = p->next;
        }
        printf("NULL\n");
    }
}

int main() {
    int n = 4;
    
    // 邻接矩阵
    GraphMatrix *gm = createMatrixGraph(n, 0);
    addEdgeMatrix(gm, 0, 1, 1);
    addEdgeMatrix(gm, 0, 2, 1);
    addEdgeMatrix(gm, 1, 3, 1);
    addEdgeMatrix(gm, 2, 3, 1);
    printMatrixGraph(gm);
    
    // 邻接表
    GraphList *gl = createListGraph(n, 0);
    addEdgeList(gl, 0, 1, 1);
    addEdgeList(gl, 0, 2, 1);
    addEdgeList(gl, 1, 3, 1);
    addEdgeList(gl, 2, 3, 1);
    printListGraph(gl);
    
    return 0;
}

六、如何选择存储结构

场景 推荐 理由
稠密图(边数接近 V²) 邻接矩阵 空间浪费可接受,操作简单
稀疏图(边数远小于 V²) 邻接表 节省空间
需要频繁判断两点是否有边 邻接矩阵 O(1)时间
需要频繁遍历邻接顶点 邻接表 只遍历实际存在的边
图结构固定,不常修改 两者均可 -
算法需要矩阵运算(如Floyd) 邻接矩阵 方便计算

七、小结

这一篇我们学习了图的两种存储结构:

存储方式 核心 优点 缺点
邻接矩阵 二维数组 简单直观,边查询O(1) 空间大O(V²)
邻接表 链表数组 节省空间O(V+E) 查询需遍历链表

邻接矩阵公式

  • 无向图:对称矩阵,对角线为0

  • 有向图:不一定对称

邻接表要点

  • 每个顶点一个链表

  • 无向图每条边存两次

  • 插入边用头插法O(1)

下一篇我们讲图的遍历:DFS和BFS。


八、思考题

  1. 一个无向图有V个顶点,邻接矩阵有多少个存储单元?邻接表大约多少个?

  2. 对于有向图,邻接表和逆邻接表有什么区别?分别有什么用途?

  3. 为什么邻接表通常用头插法而不是尾插法?

  4. 如果图中需要频繁删除边,邻接矩阵和邻接表哪个更方便?

欢迎在评论区讨论你的答案。

相关推荐
sparEE2 小时前
基础排序算法:冒泡、选择、插入、希尔
数据结构·算法·排序算法
ths5122 小时前
测试开发python中正则表达式使用总结(二)
开发语言·python·算法
不爱吃炸鸡柳2 小时前
5道经典贪心算法题详解:从入门到进阶
开发语言·数据结构·c++·算法·贪心算法
枫叶林FYL2 小时前
【自然语言处理 NLP】8.3 长文本推理评估与针在大海堆任务
人工智能·算法
智者知已应修善业2 小时前
【51单片机1,左边4个LED灯先闪烁2次后,右边4个LED灯再闪烁2次:2,接着所用灯一起闪烁3次,接着重复步骤1,如此循环。】2023-5-19
c++·经验分享·笔记·算法·51单片机
米啦啦.2 小时前
红黑树,,
数据结构·红黑树
xiaoye-duck2 小时前
《算法题讲解指南:优选算法-队列+宽搜》--70.N叉树的层序遍历,71.二叉树的锯齿形层序遍历,72.二叉树的最大宽度,73.在每个树行中找最大值
数据结构·c++·算法·队列
代码改善世界2 小时前
【C++初阶】双向循环链表:List底层结构的完整实现剖析
c++·链表·list