目录
1. 前言
在图论中,图是一种常见的数据结构,用于表示各种实际问题中的关系。图的存储方法有多种,其中邻接表存储法是一种常用且高效的表示方法。本文将详细介绍图的邻接表存储法及其实现原理,同时分析其优缺点,并提供实际应用示例,以帮助读者深入了解和应用这一图表示方法。
2. 邻接表存储法的原理
邻接表存储法是一种使用链表来表示图的结构,其核心思想是通过链表将与每个顶点相邻的顶点连接起来。对于图中的每个顶点,创建一个链表,链表中存储了与该顶点相邻的其他顶点。每个顶点结构包含两个字段:顶点的数据(通常是一个标识符或值)和指向第一个邻接边的指针(通常称为firstEdge)。
在无向图中,如果顶点A与顶点B相邻,那么在邻接表中,A的链表中会有一个指向B的边结构,同时B的链表中也会有一个指向A的边结构。在有向图中,只有一个方向上有连接,即顶点A的链表中有一个指向B的边结构,而B的链表中没有指向A的边结构。
3. 邻接表存储法的优缺点
3.1 优点
节省空间:对于稀疏图(边相对较少的图),邻接表存储法能够节省大量的内存空间,因为只存储实际存在的边,不需要占用多余的空间。
方便查找:在查找与某个顶点相邻的所有顶点时,只需要遍历该顶点的邻接链表,不需要遍历整个矩阵,提高了查找效率。
3.2 缺点
查找特定边较慢:在邻接表存储法中查找特定的边可能需要遍历整个邻接表,相对较慢,特别是对于稠密图。
内存开销:对于稠密图,邻接表存储法可能会占用较多的内存空间,因为每个顶点都需要一个链表来存储邻接边。
4. 使用邻接表存储法解决实际问题
邻接表存储法在实际问题中有广泛应用。以下是一些实际应用示例:
4.1 社交网络
社交网络中的用户关系可以用无向图表示,每个用户是一个顶点,好友关系是一条边。使用邻接表存储法表示图后,可以方便地查找某个用户的好友列表,或者查找共同好友等信息。
4.2 路径查找算法
例如广度优先搜索(BFS)和深度优先搜索(DFS)。在邻接表存储法中,使用BFS或DFS算法可以在图中查找两个顶点之间的最短路径或所有可能的路径。
5. 实现邻接表存储法
为了更好地理解邻接表存储法,我们来实现一个简单的图的邻接表表示。下面是用C语言实现的示例代码:
5.1 邻接表存储结构定义
#define MAX_VERTICES 100
typedef int VertexType;
// 定义图的边结构
typedef struct Edge {
int adjVertex; // 与边相连的当前此顶点的索引
Edge* next; // 指向下一个邻接边的指针
} Edge;
// 定义图的顶点结构
typedef struct Vertex {
VertexType data; // 顶点数据
Edge* firstEdge; // 指向第一个邻接边的指针
} Vertex;
// 定义图的邻接表结构
typedef struct Graph {
int num_vertices,num_edges; // 图中顶点的数量
Vertex vertices[MAX_VERTICES]; // 存储顶点的数组
} Graph;
5.2 初始化
// 初始化图
void initGraph(Graph* graph, int num_vertices) {
graph->num_vertices = num_vertices;
for (int i = 0; i < num_vertices; ++i) {
graph->vertices[i].data = i; // 以顶点索引作为顶点数据
graph->vertices[i].firstEdge = NULL; // 初始化顶点的邻接边链表为空
}
}
5.3 插入删除边操作
// 向图中插入一条边
void insertEdge(Graph* graph, int from, int to) {
if (from >= 0 && from < graph->num_vertices && to >= 0 && to < graph->num_vertices) {
// 创建新的边节点
Edge* newEdge = (Edge*)malloc(sizeof(Edge));
newEdge->adjVertex = to;
newEdge->next = graph->vertices[from].firstEdge;
// 更新顶点的邻接边链表
graph->vertices[from].firstEdge = newEdge;
}
}
// 删除图中的一条边
void deleteEdge(Graph* graph, int from, int to) {
if (from >= 0 && from < graph->num_vertices && to >= 0 && to < graph->num_vertices) {
Edge* prevEdge = NULL;
Edge* currentEdge = graph->vertices[from].firstEdge;
// 在from的邻接链表中查找与to相邻的边
while (currentEdge != NULL && currentEdge->adjVertex != to) {
prevEdge = currentEdge;
currentEdge = currentEdge->next;
}
// 如果找到了与to相邻的边,进行删除操作
if (currentEdge != NULL) {
if (prevEdge != NULL) {
// 删除中间或尾部的边节点
prevEdge->next = currentEdge->next;
}
else {
// 删除头部的边节点
graph->vertices[from].firstEdge = currentEdge->next;
}
// 释放删除的边节点内存
free(currentEdge);
}
// 如果是无向图,还需要在to的邻接链表中删除与from相邻的边
// 此处省略代码,读者可以自行添加
}
}
其实,对用邻接表存储的图的操作,其实就是对一条条链表操作,每个顶点对应一条链表,这条链表上存储这个顶点所有有关联的顶点信息,其实也就是将此顶点的所有边给间接表示出来了,这就是邻接表的核心。
对用邻接表存储的图的其他操作,我会在后续在图的遍历与图的应用当中用到。
6. 结论
图的邻接表存储法是一种高效节省空间的图表示方法,适用于稀疏图和需要频繁查找相邻顶点的场景。虽然它在查找特定边和内存开销方面存在一些缺点,但在实际应用中,通过合理选择存储结构和算法,可以充分发挥邻接表存储法的优势,解决各种实际问题。
通过本文的介绍,相信读者已经对图的邻接表存储法有了较深入的了解,可以在实际项目中灵活应用,提高算法效率,并优化内存空间的使用。
总之,明确一点,将图中所有顶点存在一个Vertex vertices[MAX_VERTICES]数组中,这个顶点数组中,每个顶点元素都有一个用来存储两连顶点的链表,并且这个顶点元素就是它的链表的 “头“。操作图就是操作这个顶点数组与它们的链表。