一、图的基本概念
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。
八、思考题
-
一个无向图有V个顶点,邻接矩阵有多少个存储单元?邻接表大约多少个?
-
对于有向图,邻接表和逆邻接表有什么区别?分别有什么用途?
-
为什么邻接表通常用头插法而不是尾插法?
-
如果图中需要频繁删除边,邻接矩阵和邻接表哪个更方便?
欢迎在评论区讨论你的答案。