作者:解学武
图的邻接多重表存储结构(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语言和数据结构”官方原创,由国家机构和地方版权局所签发的权威证书所保护。