作者:解学武
图的邻接多重表存储结构(C语言实现)
存储无向图(网),既可以使用邻接表结构,也可以使用本节讲解的邻接多重表结构。
以图 1a) 的无向图为例,如果用邻接表存储它,存储状态如图 1b) 所示:

图 1 邻接表存储无向图
观察图 1b) 的邻接表:
在无向图里,(Vi, Vj) 和 (Vj, Vi) 表示的其实是同一条边,比如 (V1, V2) 和 (V2, V1) 就是同一条边。无向图的邻接表存储结构中,每条边都会存储两份,比如我们可以在 V1 顶点的链表中找到 (V1, V2) 这条边,也可以在 V2 的链表中找到 (V2, V1) 这条边。
实际场景中,如果需要对无向图中的边做大量的插入或删除操作,不推荐使用邻接表存储结构,因为每条边在邻接表都存有两份,同样的操作需要处理两次。这种情况下,可以优先考虑邻接多重表存储结构。
邻接多重表存储无向图的方式,可以看作是邻接表和十字链表的结合体,具体来讲就是:将图中的所有顶点存储到顺序表(也可以用链表)中,同时为每个顶点配备一个链表,链表的各个结点中存储的都是和当前顶点有直接关联的边。
举个简单的例子,用邻接多重表存储图 1a) 的无向图,存储状态如下图所示:

图 2 邻接多重表存储无向图
观察图 2b),顺序表的各个存储空间分为 2 部分,链表中的结点空间分为 5 部分。
顺序表用来存储图中的各个顶点,各个存储空间的结构如下图所示:

图 3 顺序表中存储空间的结构
data 数据域用来存储顶点的数据;firstedge 指针域用来指向为当前顶点配备的链表。
邻接多重表中的链表用来存储和当前顶点有直接关联的边,结点的结构如下图所示:

图 4 链表中的结点结构
各个部分的含义分别是:
在邻接多重表中,很容易可以找到和目标顶点有直接关联的所有边。以图 3 中的 V1 顶点为例,在邻接多重表中查找和它直接关联的边,具体过程是:
对比图 1 和图 2 不难发现,邻接多重表和邻接表最大的不同是:对于无向图中的每个边,邻接表需要存储两份数据,而邻接多重表只需要存储一份。因此,当需要在无向图中做大量的插入或删除边的操作时,选用邻接多重表存储无向图,可以提高程序的执行效率。
构建无向图的邻接多重表结构,对应的 C 语言代码为:
声明:当前文章为本站“玩转C语言和数据结构”官方原创,由国家机构和地方版权局所签发的权威证书所保护。
以图 1a) 的无向图为例,如果用邻接表存储它,存储状态如图 1b) 所示:

图 1 邻接表存储无向图
- V1 的链表中有两个结点,记录着 (V1, V2) 和 (V1, V4) 这两条边;
- V2 的链表中有三个结点,记录着 (V2, V1)、(V2, V3) 和 (V2, V5) 这三条边;
- V3 的链表中有三个结点,记录着 (V3, V2)、(V3, V4) 和 (V3, V5) 这三条边;
- V4 的链表中有两个结点,记录着 (V4, V1) 和 (V4, V3) 这两条边;
- V5 的链表中有两个结点,记录着 (V5, V3) 和 (V5, V2) 这两条边。
在无向图里,(Vi, Vj) 和 (Vj, Vi) 表示的其实是同一条边,比如 (V1, V2) 和 (V2, V1) 就是同一条边。无向图的邻接表存储结构中,每条边都会存储两份,比如我们可以在 V1 顶点的链表中找到 (V1, V2) 这条边,也可以在 V2 的链表中找到 (V2, V1) 这条边。
实际场景中,如果需要对无向图中的边做大量的插入或删除操作,不推荐使用邻接表存储结构,因为每条边在邻接表都存有两份,同样的操作需要处理两次。这种情况下,可以优先考虑邻接多重表存储结构。
邻接多重表是什么
邻接多重表(Adjacency Multilist)是一种专门存储无向图(网)的结构。邻接多重表存储无向图的方式,可以看作是邻接表和十字链表的结合体,具体来讲就是:将图中的所有顶点存储到顺序表(也可以用链表)中,同时为每个顶点配备一个链表,链表的各个结点中存储的都是和当前顶点有直接关联的边。
举个简单的例子,用邻接多重表存储图 1a) 的无向图,存储状态如下图所示:

图 2 邻接多重表存储无向图
观察图 2b),顺序表的各个存储空间分为 2 部分,链表中的结点空间分为 5 部分。
顺序表用来存储图中的各个顶点,各个存储空间的结构如下图所示:

图 3 顺序表中存储空间的结构
data 数据域用来存储顶点的数据;firstedge 指针域用来指向为当前顶点配备的链表。
邻接多重表中的链表用来存储和当前顶点有直接关联的边,结点的结构如下图所示:

图 4 链表中的结点结构
各个部分的含义分别是:
- mark 标志域:实际场景中,可以为每个结点设置一个标志域,记录当前结点是否已经被操作过。例如遍历无向图中的所有边,借助 mark 标志域可以避免重复访问同一条边;
- ivex 和 jvec:都是数据域,分别存储边两端顶点所在顺序表中的位置下标;
- ilink 指针域:指向下一个与 ivex 顶点有直接关联的边结点;
- jlink 指针域:指向下一个与 jvex 顶点有直接关联的边节点;
- info 指针域:存储当前边的其它信息,比如存储无向网时,可以用 info 指针域存储边的权值。
在邻接多重表中,很容易可以找到和目标顶点有直接关联的所有边。以图 3 中的 V1 顶点为例,在邻接多重表中查找和它直接关联的边,具体过程是:
- 根据 V1 顶点的 firstedge 指针域,找到第一个和 V1 有直接关联的边结点 (V1, V2);
- 在 (V1, V2) 边结点中,ivex 数据域存储着 V1 顶点对应的顺序表下标,因此继续根据 ilink 指针域找到下一个边结点 (V1, V4);
- 在 (V1, V4) 边结点中,ivex 数据域存储着 V1 顶点对应的顺序表下标,但 ilink 指针域为 NULL,因此查找结束;
对比图 1 和图 2 不难发现,邻接多重表和邻接表最大的不同是:对于无向图中的每个边,邻接表需要存储两份数据,而邻接多重表只需要存储一份。因此,当需要在无向图中做大量的插入或删除边的操作时,选用邻接多重表存储无向图,可以提高程序的执行效率。
构建无向图的邻接多重表结构,对应的 C 语言代码为:
#define MAX_VERTEX_NUM 20 //图中顶点的最大数量
#define InfoType int* //边结点中info域的数据类型
#define VertexType int //顶点的数据类型
typedef enum { unvisited, visited }VisitIf; //边标志域
//表示链表中的各个结点
typedef struct EBox {
VisitIf mark; //标志域
int ivex, jvex; //边两边顶点在顺序表中的位置下标
struct EBox* ilink, * jlink; //分别指向与ivex、jvex相关的下一个边结点
InfoType* info; //边的其它信息
}EBox;
//存储图中的各个顶点
typedef struct VexBox {
VertexType data; //顶点数据域
EBox* firstedge; //指向当前顶点对应的链表
}VexBox;
//表示邻接多重表结构
typedef struct {
VexBox adjmulist[MAX_VERTEX_NUM]; //存储图中顶点的顺序表
int vexnum, edgenum; //记录图中的顶点数量和边数量
}AMLGraph;
可以根据实现场景的需要,可以修改结构体中各个成员的数据类型,必要时还可以对某些成员进行删减,怎么方便怎么来。
邻接多重表的具体实现
如下是一个完整的 C 语言程序,实现了邻接多重表结构的创建和删除,以及对无向图中指定边的插入和删除操作,附带详尽的代码注释。
#include<stdio.h>
#define MAX_VERTEX_NUM 20 //图中顶点的最大数量
#define VertexType char //顶点的数据类型
#define Status int //设定一些函数的返回值类型
typedef enum { unvisited, visited }VisitIf; //边标志域
//表示链表中的各个结点
typedef struct EBox {
VisitIf mark; //标志域
int ivex, jvex; //边两边顶点在顺序表中的位置下标
struct EBox* ilink, * jlink; //分别指向与ivex、jvex相关的下一个边结点
}EBox;
//存储图中的各个顶点
typedef struct VexBox {
VertexType data; //顶点数据域
EBox* firstedge; //指向当前顶点对应的链表
}VexBox;
//表示邻接多重表结构
typedef struct {
VexBox adjmulist[MAX_VERTEX_NUM]; //存储图中顶点的顺序表
int vexnum, edgenum; //记录图中的顶点数量和边数量
}AMLGraph;
//获取 v 顶点在顺序表中的位置下标
int LocateVex(AMLGraph* G, VertexType v);
//创建邻接多重表
Status CreateDN(AMLGraph* G);
//将(V1,V2)插入到邻接多重表中
Status InsertEdge(AMLGraph* G, VertexType V1, VertexType V2);
//从邻接多重表中删除 (V1,V2)或者(V2,V1)
Status DeleteEdge(AMLGraph* G, VertexType V1, VertexType V2);
//输出邻接多重表中包含的所有边
void PrintEdges(AMLGraph* G);
//重置各个结点中的标志域
void InitMarks(AMLGraph* G);
//释放邻接多重表中申请的堆空间
Status DeleteDN(AMLGraph* G);
int main() {
AMLGraph G;
CreateDN(&G);
PrintEdges(&G);
printf("删除 A-B 边:\n");
DeleteEdge(&G, 'A', 'B');
PrintEdges(&G);
DeleteDN(&G);
return 0;
}
int LocateVex(AMLGraph* G, VertexType v) {
int i;
//遍历一维数组,找到变量v
for (i = 0; i < G->vexnum; i++) {
if (G->adjmulist[i].data == v) {
break;
}
}
//如果找不到,输出提示语句,返回 -1
if (i > G->vexnum) {
printf("no such vertex.\n");
return -1;
}
return i;
}
Status CreateDN(AMLGraph* G) {
int i, j, k;
VertexType V1, V2;
//输入无向图的顶点数和边数
scanf("%d %d", &(G->vexnum), &(G->edgenum));
getchar();
//使用一维数组存储顶点数据,初始化指针域为NULL
for (i = 0; i < G->vexnum; i++) {
scanf("%c", &(G->adjmulist[i].data));
getchar();
G->adjmulist[i].firstedge = NULL;
}
//存储图中的所有边
for (k = 0; k < G->edgenum; k++) {
scanf("%c %c", &V1, &V2);
getchar();
InsertEdge(G, V1, V2);
}
return 1;
}
Status InsertEdge(AMLGraph* G, VertexType V1, VertexType V2) {
int V1Add = LocateVex(G, V1);
int V2Add = LocateVex(G, V2);
EBox* node = NULL, * p = NULL, * q = NULL;
if (V1Add < 0 || V2Add < 0) {
printf("输入边信息有误\n");
exit(-1);
}
//构建一个新结点
node = (EBox*)malloc(sizeof(EBox));
node->mark = unvisited;
node->ivex = V1Add;
node->jvex = V2Add;
//用头插法,将 node 结点链接到 V1 顶点的链表中
node->ilink = G->adjmulist[V1Add].firstedge;
G->adjmulist[V1Add].firstedge = node;
//用头插法,将 node 结点链接到 V2 顶点的链表中
node->jlink = G->adjmulist[V2Add].firstedge;
G->adjmulist[V2Add].firstedge = node;
return 1;
}
/*
* 删除(V1,V2) 或者(V2,V1)
* 实现思路:
* 1、从 V1 顶点的链表出发,找到目标结点的直接前驱结点;
* 2、从 V2 顶点的链表触发,找到目标结点的直接前驱结点;
* 3、将目标结点从 V1 顶点的链表中摘除,从 V2 顶点的链表中摘除
* 4、删除目标结点
*/
Status DeleteEdge(AMLGraph* G, VertexType V1, VertexType V2) {
int V1Add = LocateVex(G, V1);
int V2Add = LocateVex(G, V2);
EBox* icurNode = NULL, * ipreNode = NULL;
EBox* jcurNode = NULL, * jpreNode = NULL;
//1、从 V1 顶点的链表出发,找到目标结点的直接前驱结点;
icurNode = G->adjmulist[V1Add].firstedge;
while (icurNode && !(((icurNode->ivex == V1Add) && (icurNode->jvex == V2Add)) || ((icurNode->ivex == V2Add) && (icurNode->jvex == V1Add)))) {
ipreNode = icurNode;
if (icurNode->ivex == V1Add) {
icurNode = icurNode->ilink;
}
else
{
icurNode = icurNode->jlink;
}
}
if (!icurNode) {
printf("指定的边不存在,失败操作失败\n");
return -1;
}
//2、从 V2 顶点的链表触发,找到目标结点的直接前驱结点;
jcurNode = G->adjmulist[V2Add].firstedge;
while (jcurNode && !(((jcurNode->ivex == V1Add) && (jcurNode->jvex == V2Add)) || ((jcurNode->ivex == V2Add) && (jcurNode->jvex == V1Add)))) {
jpreNode = jcurNode;
if (jcurNode->ivex == V2Add) {
jcurNode = jcurNode->ilink;
}
else
{
jcurNode = jcurNode->jlink;
}
}
if (!jcurNode) {
printf("指定的边不存在,失败操作失败\n");
return -1;
}
//3、将目标结点从 V1 顶点的链表中摘除
if (ipreNode == NULL) {
if (icurNode->ivex == V1Add) {
G->adjmulist[V1Add].firstedge = icurNode->ilink;
}
else
{
G->adjmulist[V1Add].firstedge = icurNode->jlink;
}
}
else
{
if (ipreNode->ivex == V1Add) {
if (icurNode->ivex == V1Add) {
ipreNode->ilink = icurNode->ilink;
}
else
{
ipreNode->ilink = icurNode->jlink;
}
}
else
{
if (icurNode->ivex == V1Add) {
ipreNode->jlink = icurNode->ilink;
}
else
{
ipreNode->jlink = icurNode->jlink;
}
}
}
//3、将目标结点从 V2 顶点的链表中摘除
if (jpreNode == NULL) {
if (jcurNode->ivex == V2Add) {
G->adjmulist[V2Add].firstedge = jcurNode->ilink;
}
else
{
G->adjmulist[V2Add].firstedge = jcurNode->jlink;
}
}
else
{
if (jpreNode->ivex == V2Add) {
if (jcurNode->ivex == V2Add) {
jpreNode->ilink = jcurNode->ilink;
}
else
{
jpreNode->ilink = jcurNode->jlink;
}
}
else
{
if (jcurNode->ivex == V2Add) {
jpreNode->jlink = jcurNode->ilink;
}
else
{
jpreNode->jlink = jcurNode->jlink;
}
}
}
//4、删除目标结点
free(icurNode); //free(jcurNode),二选一
return 1;
}
//输出邻接多重表中包含的所有边
void PrintEdges(AMLGraph* G) {
int i;
EBox* p = NULL;
//重置所有结点的标志域
InitMarks(G);
for (i = 0; i < G->vexnum; i++) {
p = G->adjmulist[i].firstedge;
//如果当前结点存在,且标志域为 0
while (p && (p->mark == 0)) {
//输出该边,并将标志域置为 1
printf("%c-%c ", G->adjmulist[p->ivex].data, G->adjmulist[p->jvex].data);
p->mark = 1;
if (p->ivex == i) {
p = p->ilink;
}
else
{
p = p->jlink;
}
}
}
printf("\n");
}
//重置所有结点的标志域
void InitMarks(AMLGraph* G) {
int i;
EBox* p = NULL;
for (i = 0; i < G->vexnum; i++) {
p = G->adjmulist[i].firstedge;
while (p && (p->mark == 1)) {
p->mark = 0;
if (p->ivex == i) {
p = p->ilink;
}
else
{
p = p->jlink;
}
}
}
}
//释放邻接多重表中申请的堆空间
//直接调用DeleteEdge()删除各个结点
Status DeleteDN(AMLGraph* G) {
int i;
EBox* p = NULL, * del = NULL;
for (i = 0; i < G->vexnum; i++) {
p = G->adjmulist[i].firstedge;
while (p) {
del = p;
if (p->ivex == i) {
p = p->ilink;
}
else
{
p = p->jlink;
}
DeleteEdge(G, G->adjmulist[del->ivex].data, G->adjmulist[del->jvex].data);
}
}
return 1;
}
以图 1a) 中的无向图为例,用 A~E 表示 V1~V5,程序的执行结果为:
5 6
A B C D E
A B
A D
B C
C D
C E
B E5 6
A-D A-B B-E B-C C-E C-D
删除 A-B 边:
A-D B-E B-C C-E C-D
声明:当前文章为本站“玩转C语言和数据结构”官方原创,由国家机构和地方版权局所签发的权威证书所保护。

ICP备案: