图的存储结构相对于线性表和树来说更为复杂,因为图中的顶点具有相对概念,没有固定的位置。那我们怎么存储图的数据结构呢?我们知道,图是由(V, E)来表示的,对于无向图来说,其中 V = (v0, v1, ... , vn),E = { (vi,vj) (0 <= i, j <= n且i 不等于j)},对于有向图,E = { < vi,vj > (0 <= i, j <= n且i 不等于j)}。V是顶点的集合,E是边的集合。所以我们只要把顶点和边的集合储存起来,那么该图的所有数据就能够存储起来了。
本文只介绍两种比较常见和重要的图的存储结构:邻接矩阵和邻接表。
邻接矩阵,顾名思义,是一个矩阵,一个存储着边的信息的矩阵,而顶点则用矩阵的下标表示。对于一个邻接矩阵M,如果M(i,j)=1,则说明顶点i和顶点j之间存在一条边,对于无向图来说,M (j ,i) = M (i, j),所以其邻接矩阵是一个对称矩阵;对于有向图来说,则未必是一个对称矩阵。邻接矩阵的对角线元素都为0。下图是一个无向图和对应的临街矩阵:
图1:无向图
图2:邻接矩阵
需要注意的是,当边上有权值的时候,称之为网图,则邻接矩阵中的元素不再仅是0和1了,邻接矩阵M中的元素定义为:。
以下用C语言创建一个无向图的邻接矩阵:
头文件是:GraphStruct.h
1 /*GraphStruct.h 2 * 图的邻接矩阵存储方式,结构由顶点数量、边数量、顶点集合和边集合组成。 3 * 其中顶点集合一维数组,根据顶点的数量动态分配数组大小。 4 * 边集合是二维数组,根据顶点的数量来动态分配数组大小,对于无向图来说,该邻接矩阵是对称矩阵。 5 * 邻接矩阵比较适用于稠密图 6 */ 7 typedef char vertexType; 8 typedef int edgeType; 9 typedef struct GraphMatrix{ 10 11 int vertexNumber; // 顶点数量 12 int edgeNumber; // 边的数量 13 vertexType *vertex; // 顶点集合,动态数组 14 edgeType** edge; // 边集合,二维动态数组 15 16 } GraphMatrix; 17 18 void GraphMatrix_create(GraphMatrix *g);
该头文件包含了邻接矩阵的数据结构,结构体的成员变量包括顶点数量、边的数量、顶点集合和边的集合。为了节省空间,将顶点集合和边集设为动态数组,根据顶点数量来分配空间。
实现文件是:GraphStruct.c
1 #include <stdio.h> 2 #include <malloc.h> 3 #include"GraphStruct.h" 4 5 void GraphMatrix_create(GraphMatrix *g){ 6 7 printf("请分别输入图的顶点数量和边的数量,用空格隔开:"); 8 scanf("%d %d", &g->vertexNumber, &g->edgeNumber); //将顶点数量和边的数量存储在结构体g中相应的变量 9 g->vertex = (vertexType*)malloc(g->vertexNumber * sizeof(vertexType)); //为动态数组申请空间 10 //二维动态数组申请空间 11 g->edge = (edgeType**)malloc(g->vertexNumber * sizeof(edgeType)); 12 for (int i = 0; i < g->vertexNumber; i++){ 13 g->edge[i] = (edgeType*)malloc(g->vertexNumber * sizeof(edgeType)); 14 } 15 //初始化邻接矩阵的所有元素 16 for (int i = 0; i < g->vertexNumber; i++){ 17 for (int j = 0; j < g->vertexNumber; j++) 18 g->edge[i][j] = 0; 19 } 20 21 //输入图的信息 22 for (int k = 0; k < g->edgeNumber; k++){ 23 24 int i, j; 25 printf("请输入边(vi,vj)的下标, i和j,用空格隔开:"); 26 scanf("%d%d", &i, &j); 27 g->edge[i][j] = 1; //(i,j)和(j,i)指的是一条边 28 g->edge[j][i] = 1; 29 } 30 31 //输出图的信息 32 printf("Your graph matrix is :\n"); 33 for (int i = 0; i < g->vertexNumber; i++){ 34 for (int j = 0; j < g->vertexNumber; j++){ 35 printf("%d\t", g->edge[i][j]); 36 } 37 printf("\n"); 38 } 39
测试文件为:main.c
1 #include<stddef.h> 2 #include "GraphStruct.h" 3 4 int main(){ 5 6 GraphMatrix *gm; 7 gm = (GraphMatrix *)malloc(sizeof(GraphMatrix)); 8 GraphMatrix_create(gm); 9 10 11 return 0; 12 }
运行结果为:
图3 运行结果
对于有向图,网图的代码,只要将上面的代码稍微修改就行了,本文末尾附上代码的下载地址。
对于顶点数很多但是边数很少的图来说,用邻接矩阵显得略为“奢侈”,因为矩阵元素为1的很少,即其中的有用信息很少,但却占了很大的空间。所以下面我们来看看邻接表,以图1的无向图为例,我们先不讲理论的知识,先把图1的邻接表画出来,如图4。
图4 邻接表
作为顶点0,它的邻接顶点有1,3,4,形成的边有(0,1),(0,3)和(0,4),所以顶点0将其指出来了;对于顶点1,它的邻接顶点有0,2,4,所以顶点1将其指出来了,以此类推。他们的边没有先后顺序之分。对于边(i,j),邻接表如下:
图5 (i,j)的邻接表
左边的节点称为顶点节点,其结构体包含顶点元素和指向第一条边的指针;右边的为边节点,结构体包含边的顶点对应的下标,和指向下一个边节点的指针。对于有权值的网图,只需要在边节点增加一个权值的成员变量即可。
实现邻接表存储结构的代码如下:
头文件是:GraphStruct.h
1 /* 2 *图的另一种存储结构是邻接表 3 4 */ 5 typedef struct ListEdgeNode{ 6 int index; // 边的下标 7 struct ListEdgeNode *next; // 指向下一个节点的指针 8 }ListEdgeNode; 9 10 typedef struct ListVertexNode { 11 vertexType vertex; // 顶点 12 ListEdgeNode *fistEdge; // 指向第一条边 13 } ListVertexNode; 14 15 typedef struct GraphList{ 16 int vertexNumber; // 顶点的数量 17 int edgeNumber; // 边的数量 18 ListVertexNode *vertex; // 顶点集合,动态数组 19 }GraphList; 20 21 void GraphList_create(GraphList *g);
该文件定义的结构体如上所述,GraphList是链接表的结构体,包含了顶点数,边数和顶点集,其中顶点集根据顶点个数分配内存空间。
实现文件是:GraphStruct.c
void GraphList_create(GraphList *g){ printf("请分别输入图的顶点数量和边的数量,用空格隔开:"); scanf("%d %d", &g->vertexNumber, &g->edgeNumber); //将顶点数量和边的数量存储在结构体g中相应的变量 //为动态数组申请空间 g->vertex = (ListVertexNode*)malloc(g->vertexNumber * sizeof(ListVertexNode)); //初始化顶点指的第一条边 for (int i = 0; i < g->edgeNumber; i++){ g->vertex[i].fistEdge = NULL; } //输入图的信息 ListEdgeNode *listEdgeNode; for (int k = 0; k < g->edgeNumber; k++){ int i, j; printf("请输入边(vi,vj)的下标, i和j,用空格隔开:"); scanf("%d%d", &i, &j); //始终将插入的节点放在顶点所指的地一条边 listEdgeNode = (ListEdgeNode *)malloc(sizeof(ListEdgeNode)); listEdgeNode->index = j; listEdgeNode->next = g->vertex[i].fistEdge; g->vertex[i].fistEdge = listEdgeNode; listEdgeNode = (ListEdgeNode*)malloc(sizeof(ListEdgeNode)); listEdgeNode->index = i; listEdgeNode->next = g->vertex[j].fistEdge; g->vertex[j].fistEdge = listEdgeNode; } //输出图的信息 ListEdgeNode * len = NULL; for (int i = 0; i < g->vertexNumber; i++){ if (g->vertex[i].fistEdge != NULL) len = g->vertex[i].fistEdge; while (len!= NULL){ printf("%d --- %d\t", i,len->index); len = len->next; } printf("\n"); } }
测试文件是:main.c
1 #include<stddef.h> 2 #include "GraphStruct.h" 3 4 int main(){ 5 6 7 GraphList *gl; 8 gl = (GraphList*)malloc(sizeof(GraphList)); 9 GraphList_create(gl); 10 return 0; 11 }
运行结果为:
邻接矩阵适合于点少边多的图,而对于边少的图,可以考虑用邻接表。但是对于有向图,邻接表的表示有多种,有些更为复杂,在这里不一一阐述,有兴趣的可以跟本人交流。