目录
一:图的抽象数据类型二:图的存储结构讨论
三:图的存储结构(1)---邻接矩阵
(一)无向图
(二)有向图
(三)网
(四)实现无向网图创建
四:图的存储结构(2)---邻接表
邻接表处理办法
(一)无向图
(二)有向图
(三)带权值的网图
(四)实现无向网图
注意:上面的两种存储结构是针对顶点,下面的三种存储结构是针对边
五:图的存储结构(3)---十字链表
代码实现
六:图的存储结构(4)---邻接多重表
定义
总结
代码实现前的思考:
按照上面来实现代码
七:图的存储结构(5)---边集数组
代码实现
一:图的抽象数据类型
ADT 图(Graph)
Data
顶点的有穷非空集合和边的集合
Operation
CreateGraph(*G,V,VR):按照顶点集V和边弧集VR的定义构造图G
DestroyGraph(*G):图G存在则销毁
LocateVex(G,u):若图G中存在顶点u,则返回图中位置
GetVex(G,v):返回图中顶点v的值
PutVex(G,v,value):将图G中顶点v赋值给value
FirstAdjVex(G,*v):返回顶点v的一个邻接顶点,若顶点在G中无邻接顶点则返回空
NextAdjVex(G,v,*w):返回顶点v相对于顶点w的下一个邻接顶点,若w是v的最后一个邻接点则返回空
InsertVex(*G,v):在图G中增加新顶点v
DeleteVex(*G,v):删除图G中顶点v及其相关的弧
InsertArc(*G,v,w):在图G中添加弧<v,w>,若G是无向图,还需要添加对称弧<w,v>
DeleteArc(*G,v,w):在图G中删除弧<v,w>,若G是无向图,则需要删除对称弧<w,v>
DFSTraverse(G):对图G中进行深度优先遍历,在遍历过程对每个顶点调用
HFSTraverse(G):对图G中进行广度优先遍历,在遍历过程对每个顶点调用
endADT
二:图的存储结构讨论
对于线性表来说,是一对一的关系,所以用数组或者链表均可以简单存放。
对于树结构是一对多的关系,所以我们要将数组和链表的特性结合在一起才能更好的存放。
对于图来说,是多对多的情况,另外图上的任意一个顶点都可以被看做是第一个顶点,任一顶点的邻接点之间也不存在次序关系
如下图:实际是一个图结构,只不过顶点位置不同。
由于图的结构复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间的关系,也就是说,图不可能用简单的顺序存储结构来表示。
内存物理位置是线性的,图的元素关系是平面的。
虽然我们可以向树结构中说到的那样使用多重链表,但是我们需要先确定最大的度,然后按照这个度最大的顶点设计结点结构,若是每个顶点的度数相差较大,就会造成大量的存储单元浪费。
三:图的存储结构(1)---邻接矩阵
考虑到图是由顶点和边(弧)两部分组成的,合在一起是比较困难的,那就很自然的考虑到分为两个结构来分别存储
顶点因为不区分大小,主次,所以用一个一维数组来存储时不错的选择。
而边或弧由于是顶点与顶点之间的关系,所以我们最好使用二维数组来存储
图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
(一)无向图
其中1表示两个顶点之间存在关系,0表示无关,不存在顶点间的边。
对称矩阵:就是n阶矩阵满足a[i][j]=a[j][i](0<=i,j<=n)。即从矩阵的左上角到右下角的主对角线为轴,右上角的源与左下角相对应的元都是相等的。
根据这个矩阵,我们可以很容易的知道图中的信息。
1.我们要判定容易两顶点是否有边无边就非常容易了。
2.我们要知道某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行(或第i列)的元素之和。比如上图顶点v1的度就是1+0+1+0=2
3.求顶点vi的所有邻接点就是将矩阵第i行元素扫描一遍,arc[i][j]为1就是邻接点
(二)有向图
对于上面的无向图,二维对称矩阵似乎浪费了一半的空间。若是存放有向图,会更大程度利用起来空间
其中顶点数组是一样的和无向图,弧数组也是一个矩阵,但因为是有向图,所以这个矩阵并不对称:例如v1->v0有弧,arc[1][0]=1,而v0到v1没有弧,所以arc[0][1]=0。
另外有向图,要考虑入度和出度,顶点v1的入度为1,正好是第v1列的各数之和,顶点v1的出度为2,正好是第v2行的各数之和
(三)网
每条边上带有权的图就叫做网
这里‘∞’表示一个计算机允许的,大于所有边上权值的值
(四)实现无向网图创建
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXVEX 100 //最大顶点数
#define INFINITY 65535 //用65535表示∞
typedef char VertexType; //顶点类型,字符型A,B,C,D...
typedef int EdgeType; //边上权值类型10,15,...
typedef struct
{
VertexType vers[MAXVEX]; //顶点表
EdgeType arc[MAXVEX][MAXVEX]; //邻接矩阵,可看作边表
int numVertexes, numEdges; //图中当前的顶点数和边数
}MGraph;
void CreateMGraph(MGraph* G)
{
int i, j, k, w;
printf("please input number of vertex and edge:\n");
scanf("%d,%d", &G->numVertexes, &G->numEdges); //输入顶点数和边数
getchar(); //可以获取回车符
for (i = 0; i < G->numVertexes; i++) //读入顶点信息,建立顶点表
scanf("%c", &G->vers[i]);
getchar(); //可以获取回车符
for (i = 0; i < G->numVertexes;i++)
for (j = 0; j < G->numVertexes;j++)
G->arc[i][j] = INFINITY; //邻接矩阵初始化
for (k = 0; k < G->numEdges;k++) //读入numEdges条边,建立邻接矩阵
{
printf("input edge(vi,vj) row(i),col(j),weight(w):\n");
scanf("%d,%d,%d", &i, &j, &w); //输入边(vi,vj),以及上面的权值
getchar(); //可以获取回车符
G->arc[i][j] = w;
G->arc[j][i] = G->arc[i][j]; //因为是无向图,所有是对称矩阵
}
}
int main()
{
MGraph MG;
CreateMGraph(&MG);
system("pause");
return 0;
}
n个顶点,e条边创建无向网图,时间复杂度是O(n+n^2+e),初始化时耗费了O(n^2)
四:图的存储结构(2)---邻接表
上面的邻接矩阵是一种不错的图存储结构,便于理解,但是当我们的边数相对于顶点较少的图,这种结构是存在对存储空间的极大的浪费。
我们可以考虑使用链表来动态分配空间,避免使用数组一次分配而造成空间浪费问题。
同树中,孩子表示法,我们将结点存放入数组,对结点的孩子进行链式存储,不管有多少孩子,都不会存在空间浪费。这种思路也适用于图的存储。我们把这种数组与链表相结合的存储方法称为邻接表
邻接表处理办法
1.图中顶点用一个一维数组。当然,顶点也可以用单链表来存储,不过数组可以较容易的读取顶点信息,更加方便。另外,对于顶点数组中,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息
2.图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表
(一)无向图
这样的结构,对于我们要获得图的相关信息也是很方便。比如:
我们要获取某个顶点的度,就要去查找这个顶点的边表中结点的个数。
我们要判断vi到vj是否存在边,只需要测试vi的边表链表中是否存在结点vj的下标j即可。
我们若是要获取顶点的所有邻接点,就是对此顶点的边表进行遍历。
(二)有向图
有向图由于有方向,我们是以顶点为弧尾来存储边表的,这样很容易就可以得到每个顶点的出度。但是由于有时也需要确定顶点的入度或以顶点作为弧头的弧,我们可以建立一个有向图的逆邻接表,即对每个顶点vi都建立一个链接为vi为弧头的表
(三)带权值的网图
我们可以在边表结点定义中再增加一个weight数据域,存储权值信息即可
(四)实现无向网图
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXVEX 100 //最大顶点数
typedef char VertexType; //顶点类型,字符型A,B,C,D...
typedef int EdgeType; //边上权值类型10,15,...
typedef struct EdgeNode //边表结点
{
int adjvex; //邻接点域,存放该顶点对应的下标
int weight; //用于存放权值,对于非网图可以不需要
struct EdgeNode* next; //链域,指向下一个邻接点
}EdgeNode;
typedef struct VertexNode //顶点表结点
{
VertexType data; //顶点域,存储顶点信息
EdgeNode* firstedge; //边表头指针
}VertexNode,AdjList[MAXVEX];
typedef struct
{
AdjList adjList; //邻接表数组
int numVertexes, numEdges; //图中所存储的顶点数和边数
}GraphAdjList;
void CreateALGraph(GraphAdjList* G)
{
int i, j ,k,w;
EdgeNode *e;
printf("please input number of vertex and edge:\n");
scanf("%d,%d",&G->numVertexes,&G->numEdges); //输入顶点数和边数
getchar(); //可以获取回车符
for (i = 0; i < G->numVertexes;i++) //输入顶点信息
{
scanf("%c", &G->adjList[i].data); //输入顶点信息
G->adjList[i].firstedge = NULL; //将边表置为空
}
getchar(); //可以获取回车符
for (k = 0; k < G->numEdges;k++)
{
printf("input edge(vi,vj) vertexs series and the weight:\n");
scanf("%d,%d,%d", &i, &j,&w);
getchar();
//由于是无向图,对称矩阵,当我们设置边以后,需要在两个地方设置结点
e = (EdgeNode *)malloc(sizeof(EdgeNode));
//使用头插法将数据插入(主要是头插法方便),我们插入不需要考虑顺序,因为链表结点都是与数组顶点相连接的
e->adjvex = j;
e->next = G->adjList[i].firstedge;
e->weight = w;
G->adjList[i].firstedge = e;
e = (EdgeNode*)malloc(sizeof(EdgeNode));
e->adjvex = i;
e->next = G->adjList[j].firstedge;
e->weight = w;
G->adjList[j].firstedge = e;
}
}
int main()
{
GraphAdjList gl;
CreateALGraph(&gl);
system("pause");
return 0;
}
注意:上面的两种存储结构是针对顶点,下面的三种存储结构是针对边
五:图的存储结构(3)---十字链表
我们想要知道出度方向的顶点,可以使用邻接表,我们要了解入度就需要使用逆邻接表。但是我们既想要了解入度有想要了解出度,那么我们该如何处理?
这时就需要使用到有向图的一种存储方法:十字链表
顶点表结点结构
firstin表示入边表头指针,指向该顶点的入边表中第一个结点。
firstout表示出边表头指针,指向该顶点的出边表中第一个结点。
边表结点结构
其中tailvex是指弧起点在顶点表的下标,headvex是指弧终点在顶点表的下标。
headlink是指入边表指针域,指向终点相同的下一条边,taillink是指边表指针域,指向起点相同的下一条边。
如果是网,我们还要在其中加入权值域,来存储权值
我们可以看做横向是出度,竖向是入度
顶点的出度和入度。除了结构复杂一点外,其实创建图算法的时间复杂度和邻接表是相同的,因此很好的应用在有向图中。
注意:整张图的出度和入度是一致的(不是某个顶点,而是这张图)
代码实现
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXVEX 100 //最大顶点数
typedef char VertexType; //顶点类型,字符型A,B,C,D...
typedef int EdgeType; //边上权值类型10,15,...
typedef struct OLNode //十字链表结点
{
int tailvex, headvex; //表示两个顶点下标,构成了一条边。tailvex是箭头尾,弧尾,headvex是箭头头,是弧头
int weight; //用于存放权值,对于非网图可以不需要
struct OLNode* taillink; //十字链域,指向下一个邻接点,是出度方向
struct OLNode* headlink; //十字链域,指向下一个邻接点,是入度方向
}OLNode;
typedef struct VertexNode //顶点表结点
{
VertexType data; //顶点域,存储顶点信息
OLNode* firstin; //边表头指针,入度
OLNode* firstout; //边表头指针,出度
}VertexNode, CrossList[MAXVEX];
typedef struct
{
CrossList adjList; //邻接表数组
int numVertexes, numEdges; //图中所存储的顶点数和边数(出度指针和入度指针是和边数是一致的)
}GraphCrossList;
void CreateCrossGraph(GraphCrossList* G)
{
int i, j ,k,w;
OLNode *e;
printf("please input number of vertex and edge:\n");
scanf("%d,%d",&G->numVertexes,&G->numEdges); //输入顶点数和边数
getchar(); //可以获取回车符
for (i = 0; i < G->numVertexes;i++) //输入顶点信息
{
scanf("%c", &G->adjList[i].data); //输入顶点信息
G->adjList[i].firstin = NULL; //将边表置为空
G->adjList[i].firstout = NULL; //将边表置为空
}
getchar(); //可以获取回车符
//先循环获取出度结点,出度数和入度数是一致的
for (k = 0; k < G->numEdges;k++)
{
printf("Out-->input edge(vi,vj) vertexs series and the weight:\n");
scanf("%d,%d,%d", &i, &j,&w);
getchar();
//创建十字链表结点
e = (OLNode *)malloc(sizeof(OLNode));
//使用头插法将数据插入(主要是头插法方便),我们插入不需要考虑顺序,因为链表结点都是与数组顶点相连接的
e->tailvex = i;
e->headvex = j;
e->weight = w;
e->taillink = G->adjList[i].firstout;
G->adjList[i].firstout = e;
}
//再循环获取入度结点,在上面的出度结点建立了所有结点的基础上,我们再建立入度指针指向
//例如我们要获取v0的入度v1,我们先输入入度点0,再输入出度点1,不需要权值,权值在上面对有向图都赋值了
//我们进行第二个指针v0的入度v2,我们使用相同输入,但是开始使用头插法修改链表firstin指向
for (k = 0; k < G->numEdges; k++)
{
printf("In-->input edge(vi,vj) vertexs series:\n");
scanf("%d,%d", &i, &j);
getchar();
//根据上面的输入,我们将当前的顶点域firstin指向我们获取的新的入度域
//先找入度域
e = G->adjList[j].firstout;
while (e->headvex!=i)
e = e->taillink; //循环十字链表结点,获取我们要的入度结点
//使用头插法插入入度指针域
e->headlink = G->adjList[i].firstin;
G->adjList[i].firstin = e;
}
}
int main()
{
GraphCrossList gl;
CreateCrossGraph(&gl);
gl;
system("pause");
return 0;
}
六:图的存储结构(4)---邻接多重表
邻接多重表是对无向图的存储结构的优化
问题:
对于无向图的邻接表,我们更加关注的重点是顶点,那么是不错的选择,但是我们要是关注的是边的操作。
比如:删除一条边,那么我们的操作将变得复杂,我们需要找到这条边的两个顶点,方便去其链表中删除所表示的边。稍微有点麻烦。
改进:
我们只出现无向图中对应条数的边表结点,其他的结构,我们全部由指针来联系,所以当我们想要删除一条边时,就只需要删除对应的边表结点。指向她的指针会置为空,他自己产生的指针会消失。就完成了对边的操作。
例如上图,我们若是使用邻接表:是要10个顶点结点去表示5条边,而我们若是使用邻接多重表,只需要5个边结点即可。删除一条边就不存在重复操作
定义
邻接多重表结构
其中ivex和jvex是指某条边依附的两个顶点在顶点表中的下标。ilink指向依附顶点ivex的下一条边,jlink指向依附顶点jvex的下一条边。
如上图有4个顶点和5条边,先将边表结点画出来。由于是无向图,所以ivex,jvex正反过来都可以,为了绘图方便,都将ivex值设置的与一旁的顶点下标相同
1.将边表结点画出来
2.开始连线
首先连线的(1)(2)(3)(4)是将顶点的firstedge指向一条边,顶点下标要与ivex的值相同。
接着,由于顶点v0的(v0,v1)边的邻边有(v0,v3)和(v0,v2)。因此(5)(6)的连线就是满足指向下一条依附于顶点v0的边的目标,注意ilink指向的结点的jvex(ivex)一定要与它本身的ivex的值相同。
注意ilink指向的结点的jvex(ivex)一定要与它本身的ivex的值相同。而且为了方便,我们和jvex相同,那么全部指向都要与之一样
同理,连线(7)就是指(v1,v0)这条边,它是相当于顶点v1指向(v1,v2)边后的下一条。
v2有三条边依附,所以(3)之后就有了(8)(9)。
连线(10)就是顶点v3在连线(4)之后的下一条边
左图一共有5条边,所以右图有10条连线,完全符合预期。
总结
邻接多重表与邻接表的差别,仅仅是在于同一条边在邻接表中用两个边表结点表示,而在邻接多重表中只有一个结点。这样对边的操作就方便多了,
若要删除左图的(v0,v2)这条边,只需要将右图的(6)(9)的链接指向改为^即可。
代码实现前的思考:
MMP,怎么就想不出来用什么方法去存储这些边呢?顶点和边不对应呀....吐血....,去网上找找其他关于邻接多重表的信息吧
数据结构之图(2-2)【邻接多重表】适用于无向图
无向图的邻接多重表存储结构
这两篇文章来了思路,将表的结构修改一下,我们只需要保证邻接多重表的根本,就是创建边表,而不是创建顶点表,因为顶点表是边表的两倍,在删除时会导致重复操作,而我们的边表只需要创建对应边数的结点,删除某条边,就删除对应结点即可。不需要重复操作。
上面这种表示方法,但是好像没说清楚为啥这么排列。
于是决定测试将对应顶点的邻接边全部放入顶点后面
步骤一:先排序第一个顶点的所有邻接边
步骤二:我们再排序第二个顶点的所有邻接边
步骤三:我们接着排序第三个顶点的所有邻接边
步骤四:开始排序最后一个顶点所有邻接边
通过上面排序,我们可以获取所有邻接点和邻接边
按照上面来实现代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXVEX 100 //最大顶点数
typedef char VertexType; //顶点类型,字符型A,B,C,D...
typedef int EdgeType; //边上权值类型10,15,...
typedef struct ENode //十字链表结点
{
int ivex, jvex; //表示两个顶点下标,构成了一条边。
int weight; //用于存放权值,对于非网图可以不需要
struct ENode* ilink; //邻接多重表,指向下一个邻接点,
struct ENode* jlink; //邻接多重表,指向下一个邻接点,
}ENode;
typedef struct VertexNode //顶点表结点
{
VertexType data; //顶点域,存储顶点信息
ENode* firstedge; //边表头指针
}VertexNode, AMLList[MAXVEX];
typedef struct
{
AMLList adjList; //邻接多重表数组
int numVertexes, numEdges; //图中所存储的顶点数和边数
}AMLGraphList;
//根据ivex找到第几行,我们去前面几行查找,jvex获取下标
ENode* GetNode(AMLGraphList* G,int ivex,int jvex)
{
int i;
ENode *e;
for (i = ivex-1; i >=0;i--)
{
e = G->adjList[i].firstedge;
while (e)
{
if (e->jvex==jvex)
{
return e;
}
e = e->ilink;
}
}
return NULL;
}
void CreateAMLGraph(AMLGraphList* G)
{
int i, j, k, w,flag;
ENode *e;
ENode *tempNode;
printf("please input number of vertex and edge:\n");
scanf("%d,%d", &G->numVertexes, &G->numEdges); //输入顶点数和边数
getchar(); //可以获取回车符
for (i = 0; i < G->numVertexes; i++) //输入顶点信息
{
scanf("%c", &G->adjList[i].data); //输入顶点信息
G->adjList[i].firstedge = NULL; //将边表置为空
}
getchar(); //可以获取回车符
//先循环获取边信息。创建所有边信息,放在对应的顶点后面,
for (k = 0; k < G->numEdges; k++)
{
printf("Out-->input edge(vi,vj) vertexs series and the weight:\n");
scanf("%d,%d,%d", &i, &j, &w);
getchar();
//创建邻接多重表结点
e = (ENode *)malloc(sizeof(ENode));
//使用头插法将数据插入(主要是头插法方便),我们插入不需要考虑顺序,因为链表结点都是与数组顶点相连接的
e->ivex = i;
e->jvex = j;
e->weight = w;
e->ilink = G->adjList[i].firstedge;
e->jlink = NULL;
G->adjList[i].firstedge = e;
}
//开始连接多张表之间的关系,并且判断ilink和jlink,从第二个顶点开始
for (i = 1; i < G->numVertexes;i++)
{
e = G->adjList[i].firstedge;
if (e)
{
flag = 0; //用来标识是不是最后一个结点
while (e&&!flag) //需要将最后一个单独处理
{
if (e->ilink == NULL)
{
tempNode = GetNode(G, i, e->ivex);
e->ilink = tempNode;
flag = 1;
}
tempNode = GetNode(G, i, e->jvex);
e->jlink = tempNode;
e = e->ilink;
}
//e = GetNode(G, i, e->jvex); //处理最后一个
}
else
G->adjList[i].firstedge = GetNode(G, i, i);
}
}
int main()
{
AMLGraphList gl;
CreateAMLGraph(&gl);
gl;
system("pause");
return 0;
}
七:图的存储结构(5)---边集数组
边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息。
这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成。
结构
表现
如上图所示,边集数组关注的是边的集合,在边集数组中要查找一个顶点的度需要扫描整个边数组,效率并不高。
因此它更适合对边依次进行处理的操作,而不适合对顶点相关的操作。
代码实现
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXVEX 100 //最大顶点数
typedef char VertexType; //顶点类型,字符型A,B,C,D...
typedef int EdgeType; //边上权值类型10,15,...
typedef struct VertexNode //顶点表结点
{
VertexType data; //顶点域,存储顶点信息
}VertexNode, VertexList[MAXVEX];
typedef struct EdgeNode //边集表结点
{
int begin, end, weight;
}EdgeNode, EdgeList[MAXVEX];
typedef struct
{
VertexList vexList; //顶点表
EdgeList edgeList; //边集表
int numVertexes, numEdges; //图中所存储的顶点数和边数
}EdgeGraphList;
void CreateEdgeGraph(EdgeGraphList* G)
{
int i, j ,k,w;
printf("please input number of vertex and edge:\n");
scanf("%d,%d",&G->numVertexes,&G->numEdges); //输入顶点数和边数
getchar(); //可以获取回车符
//获取顶点数组信息
for (i = 0; i < G->numVertexes;i++) //输入顶点信息
{
scanf("%c", &G->vexList[i].data); //输入顶点信息
}
//获取边数组
for (k = 0; k < G->numEdges;k++)
{
printf("input edge(vi,vj) vertexs series and the weight:\n");
scanf("%d,%d,%d", &i, &j,&w);
getchar();
//由于是无向图,对称矩阵,当我们设置边以后,需要在两个地方设置结点
G->edgeList[k].begin = i;
G->edgeList[k].end = j;
G->edgeList[k].weight = w;
}
}
int main()
{
EdgeGraphList gl;
CreateEdgeGraph(&gl);
gl;
system("pause");
return 0;
}