在数据结构中图算是个较为难理解的结构形式了。
大致我们可以分为两个大类:
1、通过数组实现
2、通过链表实现
而链表的实现方式还可以继续细分:邻接表、邻接多重表、十字链表
所以关于图的结构的存储这里我们介绍四种基本形式:
1、邻接矩阵(数组)
2、邻接表(链表)
3、邻接多重表(链表)
4、十字链表(链表)
邻接矩阵
使用数组存储图时,需要使用两个数组,一个数组存放图中顶点本身的数据(一维数组),另外一个数组用于存储各顶点之间的关系(二维数组)。
不同类型的图,存储的方式略有不同,根据图有无权,可以将图划分为两大类:图和网 。
注意:
1、图,包括无向图和有向图;
2、网,是指带权的图,包括无向网和有向网。
3、存储方式的不同,指的是:在使用二维数组存储图中顶点之间的关系时,如果顶点之间存在边或弧,在相应位置用 1 表示,反之用 0 表示;
3、如果使用二维数组存储网中顶点之间的关系,顶点之间如果有边或者弧的存在,在数组的相应位置存储其权值;反之用 0 表示。
邻接矩阵结构体设计:
我们先默认存储的数据是int类型,其实做好做成宏定义,这样可以随时修改使用的类型
typedef enum{ //枚举图的 4 种类型 有向图 无向图 有向网 无向网
DG,DN,UDG,UDN
}GraphKind;
typedef struct {
int adj; //对于无权图,用 1 或 0 表示是否相邻;对于带权图,直接为权值。
char * info; //弧或边额外含有的信息指针
}ArcCell,AdjMatrix[20][20];
typedef struct {
int vexs[20]; //存储图中顶点数据
AdjMatrix arcs; //二维数组,记录顶点之间的关系
int vexnum; //记录顶点数
int arcnum; //记录图的顶点数和弧(边)数
GraphKind kind; //记录图的种类
}MGraph;
例如,存储图中的无向图(B)时,除了存储图中各顶点本身具有的数据外,还需要使用二维数组存储任意两个顶点之间的关系。
由于(B)为无向图,各顶点没有权值,所以如果两顶点之间有关联,相应位置记为 1 ;反之记为 0 。构建的二维数组如下图所示。
在此二维数组中,每一行代表一个顶点,依次从 V1 到 V5 ,每一列也是如此。比如 arcs[0][1] = 1 ,表示 V1 和 V2 之间有边存在;而 arcs[0][2] = 0,说明 V1 和 V3 之间没有边。
对于无向图来说,二维数组构建的二阶矩阵,实际上是对称矩阵,在存储时就可以采用压缩存储的方式存储下三角或者上三角。
通过二阶矩阵,可以直观地判断出各个顶点的度,为该行(或该列)非 0 值的和。例如,第一行有两个 1,说明 V1 有两个边,所以度为 2。
存储有向图(A)时,对应的二维数组如下图所示:
如,arcs[0][1] = 1 ,证明从 V1 到 V2 有弧存在。且通过二阶矩阵,可以很轻松得知各顶点的出度和入度,出度为该行非 0 值的和,入度为该列非 0 值的和。
例如,V1 的出度为第一行两个 1 的和,为 2 ; V1 的入度为第一列中 1 的和,为 1 。所以 V1 的出度为 2 ,入度为 1 ,度为两者的和 3 。
注意:在此程序中,构建无向网和有向网时,对于之间没有边或弧的顶点,相应的二阶矩阵中存放的是 0。目的只是为了方便查看运行结果,而实际上如果顶点之间没有关联,它们之间的距离应该是无穷大(∞)。
实现代码:
#include <stdio.h>
typedef enum{ //枚举图的 4 种类型 有向图 无向图 有向网 无向网
DG,DN,UDG,UDN
}GraphKind;
typedef struct {
int adj; //对于无权图,用 1 或 0 表示是否相邻;对于带权图,直接为权值。
char * info; //弧或边额外含有的信息
}ArcCell,AdjMatrix[20][20];
typedef struct {
int vexs[20]; //存储图中顶点数据
AdjMatrix arcs; //二维数组,记录顶点之间的关系
int vexnum; //记录顶点数
int arcnum; //记录图的顶点数和弧(边)数
GraphKind kind; //记录图的种类
}MGraph;
//根据顶点本身数据,判断出顶点在二维数组中的位置
int LocateVex(MGraph * G,int v){
int i;
//遍历一维数组,找到变量v
for (i = 0; i < G->vexnum; i++) {
if (G->vexs[i] == v) {
break;
}
}
//如果找不到,输出提示语句,返回-1
if (i > G->vexnum) {
printf("no such vertex.\n");
return -1;
}
return i;
}
//构造有向图
void CreateDG(MGraph *G){
//输入图含有的顶点数和弧的个数
scanf("%d,%d",&(G->vexnum),&(G->arcnum));
//依次输入顶点本身的数据
int i, j;
for (i = 0; i < G->vexnum; i++) {
scanf("%d",&(G->vexs[i]));
}
//初始化二维矩阵,全部归0,指针指向NULL
for (i = 0; i < G->vexnum; i++) {
for (j = 0; j < G->vexnum; j++) {
G->arcs[i][j].adj = 0;
G->arcs[i][j].info = NULL;
}
}
//在二维数组中添加弧的数据
for (i = 0; i < G->arcnum; i++) {
int v1, v2;
//输入弧头和弧尾
scanf("%d,%d",&v1,&v2);
//确定顶点位置
int n = LocateVex(G, v1);
int m = LocateVex(G, v2);
//排除错误数据
if (m == -1 || n == -1) {
printf("没有该顶点\n");
return;
}
//将正确的弧的数据加入二维数组
G->arcs[n][m].adj=1;
}
}
//构造无向图
void CreateDN(MGraph *G){
scanf("%d,%d",&(G->vexnum),&(G->arcnum));
int i, j;
for (i = 0; i < G->vexnum; i++) {
scanf("%d",&(G->vexs[i]));
}
for (i = 0; i < G->vexnum; i++) {
for (j = 0; j < G->vexnum; j++) {
G->arcs[i][j].adj = 0;
G->arcs[i][j].info = NULL;
}
}
for (i = 0; i < G->arcnum; i++) {
int v1,v2;
scanf("%d,%d",&v1,&v2);
int n = LocateVex(G, v1);
int m = LocateVex(G, v2);
if (m == -1 || n == -1) {
printf("没有该顶点\n");
return;
}
G->arcs[n][m].adj = 1;
G->arcs[m][n].adj = 1;//无向图的二阶矩阵沿主对角线对称
}
}
//构造有向网,和有向图不同的是二阶矩阵中存储的是权值。
void CreateUDG(MGraph *G){
scanf("%d,%d",&(G->vexnum),&(G->arcnum));
int i, j;
for (i = 0; i<G->vexnum; i++) {
scanf("%d",&(G->vexs[i]));
}
for (i = 0; i < G->vexnum; i++) {
for (j = 0; j < G->vexnum; j++) {
G->arcs[i][j].adj = 0;
G->arcs[i][j].info = NULL;
}
}
for (i=0; i<G->arcnum; i++) {
int v1,v2,w;
scanf("%d,%d,%d",&v1,&v2,&w);
int n = LocateVex(G, v1);
int m = LocateVex(G, v2);
if (m == -1 || n == -1) {
printf("没有该顶点\n");
return;
}
G->arcs[n][m].adj=w;
}
}
//构造无向网。和无向图唯一的区别就是二阶矩阵中存储的是权值
void CreateUDN(MGraph* G){
scanf("%d,%d",&(G->vexnum),&(G->arcnum));
int i, j;
for (i = 0; i < G->vexnum; i++) {
scanf("%d",&(G->vexs[i]));
}
for (i = 0; i < G->vexnum; i++) {
for (j = 0; j < G->vexnum; j++) {
G->arcs[i][j].adj = 0;
G->arcs[i][j].info = NULL;
}
}
for (i=0; i<G->arcnum; i++) {
int v1,v2,w;
scanf("%d,%d,%d",&v1,&v2,&w);
int m=LocateVex(G, v1);
int n=LocateVex(G, v2);
if (m == -1 ||n == -1) {
printf("没有该顶点\n");
return;
}
G->arcs[n][m].adj = w;
G->arcs[m][n].adj = w;//矩阵对称
}
}
void CreateGraph(MGraph *G){
//选择图的类型
scanf("%d",&(G->kind));
//根据所选类型,调用不同的函数实现构造图的功能
switch (G->kind) {
case DG:
return CreateDG(G);
break;
case DN:
return CreateDN(G);
break;
case UDG:
return CreateUDG(G);
break;
case UDN:
return CreateUDN(G);
break;
default:
break;
}
}
//输出图存储数据的函数
void PrintGrapth(MGraph G)
{
int i, j;
for (i = 0; i < G.vexnum; i++)
{
for (j = 0; j < G.vexnum; j++)
{
printf("%d ", G.arcs[i][j].adj);
}
printf("\n");
}
}
int main() {
MGraph G;//建立一个图的变量
CreateGraph(&G);//调用创建函数,传入地址参数
PrintGrapth(G);//输出图的二阶矩阵
return 0;
}
可以自行根绝录入信息构建需要的的图型结构。
邻接表
使用邻接表存储图时,对于图中的每一个顶点和它相关的邻接点,都存储到一个链表中。
每个链表都配有头结点,头结点的数据域不为NULL,而是用于存储顶点本身的数据;后续链表中的各个结点存储的是当前顶点的所有邻接点。
所以,采用邻接表存储图时,有多少顶点就会构建多少个链表,为了便于管理这些链表,常用的方法是将所有链表的链表头按照一定的顺序存储在一个数组中(也可以用链表串起来)。
在邻接表中,每个链表的头结点和其它结点的组成成分有略微的不同。
头结点需要存储每个顶点的数据和指向下一个结点的指针,由两部分构成:而在存储邻接点时,由于各个顶点的数据都存储在数组中,所以每个邻接点只需要存储自己在数组中的位置下标即可,还需要一个指向下一个结点的指针。
除此之外,如果存储的是网,还需要一个记录权值的信息域。所以表头结点和其它结点的构造分别为:
注意:info 域对于无向图来说,本身不具备权值和其它相关信息,就可以根据需要将之删除。
例如,当存储下图中(A)所示的有向图时,构建的邻接表如(B)所示:
邻接表存储图的存储结构为:
typedef enum{DG,DN,UDG,UDN}GraphKind; //枚举图的 4 种类型 有向图 无向图 有向网 无向网
typedef struct ArcNode{
int adjvex; //邻接点在数组中的位置下标
struct ArcNode * nextarc; //指向下一个邻接点的指针
int * info; //信息域
}ArcNode;
typedef struct VNode{
int data; //顶点的数据域
ArcNode * firstarc; //指向邻接点的指针
}VNode,AdjList[20]; //存储各链表头结点的数组
typedef struct {
AdjList vertices; //图中顶点及各邻接点数组
int vexnum,arcnum; //记录图中顶点数和边或弧数
GraphKind kind; //记录图的种类
}ALGraph;
这里我们做一个有向网邻接表的例子:
#include <stdio.h>
typedef enum{DG,DN,UDG,UDN}GraphKind; //枚举图的 4 种类型 有向图 无向图 有向网 无向网
typedef struct ArcNode{
int adjvex; //邻接点在数组中的位置下标
struct ArcNode * nextarc; //指向下一个邻接点的指针
int * info; //信息域
}ArcNode;
typedef struct VNode{
int data; //顶点的数据域
ArcNode * firstarc; //指向邻接点的指针
}VNode,AdjList[20]; //存储各链表头结点的数组
typedef struct {
AdjList vertices; //图中顶点及各邻接点数组
int vexnum,arcnum; //记录图中顶点数和边或弧数
GraphKind kind; //记录图的种类
}ALGraph;
int LocateVex(ALGraph *G,int v){
//定位函数
int i;
for(i = 0;i<G->vexnum;i++){
if(v == G->vertices[i].data){
return i;
}
}
}
void CreateUDG(ALGraph *G){
ArcNode *p,*q;
int i,j,k,v1,v2;
printf("分别输入顶点个数和边的数目:\n");
scanf("%d,%d",&G->vexnum,&G->arcnum);
printf("分别输入各个顶点值:\n");
for(i=0;i<G->vexnum;i++){
scanf("%d",&G->vertices[i].data);
G->vertices[i].firstarc=NULL;//初始化
}
printf("分别输入各条边的两个顶点:\n");
for(k=0;k<G->arcnum;k++){
scanf("%d,%d",&v1,&v2);
//定位
i=LocateVex(G, v1);
j=LocateVex(G, v2);
p=(ArcNode*)malloc(sizeof(ArcNode));//申请一个结点
p->adjvex=j;p->nextarc=NULL;//赋值
p->nextarc=G->vertices[i].firstarc;//连接结点
G->vertices[i].firstarc=p;//连接结点
q=(ArcNode*)malloc(sizeof(ArcNode));
q->adjvex=i;q->nextarc=NULL;
q->nextarc=G->vertices[j].firstarc;
G->vertices[j].firstarc=q;
}
}
void PrintUDG(ALGraph G){
//输出邻接表
int i,j;
for(i=0;i<G.vexnum;i++){
printf("%d:",i);
ArcNode *p;
p=G.vertices[i].firstarc;
while(p!=NULL){
printf("->%d",p->adjvex);
p=p->nextarc;
}
printf("\n");
}
}
int main(){
ALGraph G;
CreateUDG(&G);
PrintUDG(G);
return 0;
}
邻接表计算顶点的度
使用邻接表存储无向图时,各顶点的度为各自链表中包含的结点数;存储有向图时,各自链表中具备的结点数为该顶点的出度。求入度时,需要遍历整个邻接表中的结点,统计数据域和该顶点数据域相同的结点的个数,即为顶点的入度。
对于求有向图中某结点的入度,还有一种方法就是再建立一个逆邻接表,此表只用于存储图中每个指向该顶点的所有的顶点在数组中的位置下标。例如,构建上一个图中的有向图和对应的邻接表的的逆邻接表,结果为:
对于具有 n 个顶点和 e 条边的无向图,邻接表中需要存储 n 个头结点和 2e 个表结点。在图中边或者弧稀疏的时候,使用邻接表要比前一节介绍的邻接矩阵更加节省空间。
十字链表
十字链表存储的对象是有向图或者有向网。
同邻接表相同的是,图(网)中每个顶点各自构成一个链表,为链表的首元结点。
同时,对于有向图(网)中的弧来说,有弧头和弧尾。一个顶点所有的弧头的数量即为该顶点的入度,弧尾的数量即为该顶点的出度。每个顶点构成的链表中,以该顶点作为弧头的弧单独构成一个链表,以该顶点作为弧尾的弧也单独构成一个链表,两个链表的表头都为该顶点构成的头结点。
这样,由每个顶点构建的链表按照一定的顺序存储在数组中,就构成了十字链表。
所以,十字链表中由两种结点构成:顶点结点和弧结点。各自的结构构成如下图所示:
弧结点中, tailvex 和 headvex 分别存储的是弧尾和弧头对应的顶点在数组中的位置下标; hlink 和 tlink 为指针域,分别指向弧头相同的下一个弧和弧尾相同的下一个弧; info 为指针域,存储的是该弧具有的相关信息,例如权值等。
顶点结点中,data 域存储该顶点含有的数据; firstin 和 firstout 为两个指针域,分别指向以该顶点为弧头和弧尾的首个弧结点。
例如,使用十字链表存储有向图 (A) ,构建的十字链表如图 (B) 所示,构建代码实现为:
typedef struct ArcBox{
int tailvex,headvex; //弧尾、弧头对应顶点在数组中的位置下标
struct ArcBox *hlik,*tlink; //分别指向弧头相同和弧尾相同的下一个弧
int *info; //存储弧相关信息的指针
}ArcBox;
typedef struct VexNode{
int data; //顶点的数据域
ArcBox *firstin,*firstout; //指向以该顶点为弧头和弧尾的链表首个结点
}VexNode;
typedef struct {
VexNode xlist[20]; //存储顶点的一维数组
int vexnum,arcnum; //记录图的顶点数和弧数
}OLGraph;
int LocateVex(OLGraph * G,int v){
int i;
//遍历一维数组,找到变量v
for (i = 0; i < G->vexnum; i++) {
if (G->xlist[i].data == v) {
break;
}
}
//如果找不到,输出提示语句,返回 -1
if (i > G->vexnum) {
printf("no such vertex.\n");
return -1;
}
return i;
}
//构建十字链表函数
void CreateDG(OLGraph *G){
int i,k;
//输入有向图的顶点数和弧数
scanf("%d,%d",&(G->vexnum),&(G->arcnum));
//使用一维数组存储顶点数据,初始化指针域为NULL
for (i = 0; i < G->vexnum; i++) {
scanf("%d",&(G->xlist[i].data));
G->xlist[i].firstin = NULL;
G->xlist[i].firstout = NULL;
}
//构建十字链表
for (k = 0;k < G->arcnum; k++) {
int v1, v2;
scanf("%d,%d",&v1, &v2);
//确定v1、v2在数组中的位置下标
int i=LocateVex(G, v1);
int j=LocateVex(G, v2);
//建立弧的结点
ArcBox * p=(ArcBox*)malloc(sizeof(ArcBox));
p->tailvex = i;
p->headvex = j;
//采用头插法插入新的p结点
p->hlik = G->xlist[j].firstin;
p->tlink = G->xlist[i].firstout;
G->xlist[j].firstin = G->xlist[i].firstout = p;
}
}
注意:
1、对于链表中的各个结点来说,由于表示的都是该顶点的出度或者入度,所以结点之间没有先后次序之分,程序中构建链表对于每个新初始化的结点采用头插法进行插入。
2、十字链表中计算顶点的度,采用十字链表表示的有向图,在计算某顶点的出度时,为 firstout 域链表中结点的个数;入度为 firstin 域链表中结点的个数。
邻接多重表
使用邻接表解决在无向图中删除某两个结点之间的边的操作时,由于表示边的结点分别处在两个顶点为头结点的链表中,所以需要都找到并删除,操作比较麻烦。处理类似这种操作,使用邻接多重表会更合适。
邻接多重表可以看做是邻接表和十字链表的结合体。和十字链表唯一不同的是顶点结点和表结点的结构组成不同;
同邻接表相比,不同的地方在于邻接表表示无向图中每个边都用两个结点,分别在两个不同链表中;而邻接多重表表示无向图中的每个边只用一个结点。
邻接多重表的顶点结点和表结点的构成如图所示:
表结点构成:
- mark 为标志域,作用是标记某结点是否已经被操作过,例如在遍历各结点时, mark 域为 0 表示还未遍历;mark 域为 1 表示该结点遍历过;
- ivex 和 jvex 分别表示该结点表示的边两端的顶点在数组中的位置下标; ilink 指向下一条与 ivex 相关的边;
- jlink 指向下一条与 jvex 相关的边;
- info 指向与该边相关的信息。
顶点结点构成:
- data 为该顶点的数据域;
- firstedge 为指向第一条跟该顶点有关系的边。
例如,使用邻接多重表表示图 中左边的无向图时,与之相对应的邻接多重表如图右侧所示。
邻接多重表的代码实现:
typedef enum { //边标志域
unvisited,visited
}VisitIf;
typedef struct EBox{
VisitIf mark; //标志域
int ivex,jvex; //边两边顶点在数组中的位置下标
struct EBox * ilink,*jlink; //分别指向与ivex、jvex相关的下一个边
int *info; //边包含的其它的信息域的指针
}EBox;
typedef struct VexBox{
int data; //顶点数据域
EBox * firstedge; //顶点相关的第一条边的指针域
}VexBox;
typedef struct {
VexBox adjmulist[20]; //存储图中顶点的数组
int vexnum,degenum; //记录途中顶点个数和边个数的变量
}AMLGraph;
图的三种链式存储结构注意事项
图的三种链式存储结构分别是:邻接表、十字链表和邻接多重表。
区别:
1、邻接表适用于所有的图结构,无论是有向图(网)还是无向图(网),存储结构较为简单,但是在存储一些问题时,例如计算某顶点的度,需要通过遍历的方式自己求得。
2、十字链表适用于有向图(网)的存储,使用该方式存储的有向图,可以很容易计算出顶点的出度和入度,只需要知道对应链表中的结点个数即可。
3、邻接多重表适用于无向图(网)的存储,该方式避免了使用邻接表存储无向图时出现的存储空间浪费的现象,同时相比邻接表存储无向图,更方便了某些边操作(遍历、删除等)的实现。