ADT 图(Graph)
Data
顶点的有穷非空集合和边的集合。多对多结构。
Operation
CreateGraph(*G,V,VR):按照顶点集V和边弧集VR的定义构造图G。
DestoryGraph(*G):图G存在便销毁。
LocateVex(G,u):若图G中存在顶点u,则返回图中的位置。
GetVex(G,v):返回图G中顶点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中增添弧,若G是无向图,还需要增添对称弧。
DeleteArc(*G,v,w):在图G中删除弧,若G是无向图,则还删除对称弧。
DFSTraverse(G):对图G中进行深度优先遍历,在遍历过程对每个顶点调用。
HFSTraverse(G):对图G中进行广度优先遍历,在遍历过程对每个顶点调用。
endADT
不能用简单的顺序存储结构表示,也不能用多重链表结构,因为有空间的浪费。下面介绍5种不同的存储结构:
图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
假设图G有n个顶点,则邻接矩阵式一个 n∗n 方阵,定义为: arc[i][j]=1,若(vi,vj)∈E 或 <vi,vj>∈E ,反之 arc[i][j]=0 。而无向图的边数组是一个对称矩阵。
图反映的信息方便:判定任意两结点是否有边;知道某结点的度,其实就是这个顶点 vi 在邻接矩阵中第i行/列的元素之和;求顶点的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1的就是该顶点的邻接点。
注意:有向图中顶点的入度是该顶点列各数之和,出度是该顶点行各数之和。
设图G是个网图,有n个结点,则邻接矩阵是一个 n∗n 方阵,定义为: arc[i][j]=Wij ,若 (vi,vj)∈E 或 <vi,vj>∈E ; arc[i][j]=0 ,若 i=j ; arc[i][j]=∞ ,反之。这里的 Wij 表示 (vi,vj) 或 (vi,vj) 的权值。还有 ∞ 是指计算机用一个不可能的值来代替不存在。
首先来看邻接矩阵存储的结构,代码如下:
typedef char VertexType; //顶点类型
typedef int EdgeType; //边上的权值类型
#define MAXVER 100; //最大顶点数
#define INFINITY 65535; //用该数来代替无穷大
type struct{
VertexType vexs[MAXVER]; //顶点表
EdgeType arc[MAXVER][MAXVER]; //邻接矩阵,边表
int numVertexes,numEdges; //图中当前顶点数和边数
}MGraph;
有了这个结构,如何构造一个图呢?其实就是给顶点表和边表输入数据的过程,下面是构建无向网图的创建代码:
/*构建无向网图的邻接矩阵表示*/
void CreateMGraph(MGraph *G){
int i,j,k,w;
printf("输入顶点数和边数:\n");
scanf("%d,%d",&G->numVertexes,&G->numEdges); //输入顶点数和边数
for(i=0;inumVertexes;i++)
scanf(&G->vexs[i]); //读入顶点信息,建立顶点表
for(i=0;inumVertexes;i++)
for(j=0;jnumVertexes;j++)
G->arc[i][j]=INFINITY; //邻接矩阵初始化
for(k=0;knumEdges;k++){ //给边表赋值
printf("输入边(vi,vj)上的下标i,下标j和权值w:\n");
scanf("%d,%d,%d",&i,&j,&w); //输入边(vi,vj)上的权值w
G->arc[i][j]=w;
G->arc[j][i]=G->arc[i][j]; //无向图,矩阵对称
}
}
n个顶点和e条边的无向网图创建的时间复杂度为 O(n+n2+e) ,其中,对邻接矩阵的初始化耗费了 O(n2) 的时间。
对于边数相对顶点较少的图,邻接矩阵这种结构存在对存储空间的极大浪费。考虑另一种存储结构,可考虑对边或弧使用链式存储的方式来避免空间浪费问题。
数组于链表相结合的存储方法称为邻接表(Adjacency List)。
具体的处理方法为:
1.图中顶点用一个一维数组存储,当然顶点也可用链表存储,不过数组更方便读取顶点信息。另外,对于顶点数组,每个数据元素还要存储指向第一个邻接点的指针,以便查找顶点的边信息。
2。图中每个顶点 vi 的所有邻接点构成一个线性表,由于邻接点个数不定,所以用单链表存储,无向图称为顶点 vi 的边表,有向图则成为顶点 vi 作为弧尾的出边表。
根据图获得信息:某顶点的度为该顶点边表中结点的个数;判断两顶点是否有边,只需测试顶点的边表中是否有该顶点的下标即可;求顶点的邻接点,只需对该顶点的边表进行遍历。
注意:若是有向图,以顶点为弧尾来存储边表,这样就能得到该顶点的出度。有时也为了能方便确定顶点入度,以顶点为弧头的弧,建立一个有向图的逆邻接表,及对每个顶点都建立一个链接为顶点为弧头的表。
对于带权的网图,也可在边表结点定义中再增加一个weight的数据域,存储权值信息。
下面详细介绍邻接表的结点定义的代码:
typedef char VertexType; //顶点类型
typedef int EdgeType; //边上的权值类型
type struct EdgeNode{ //边表结点
int adjvex; //邻接点域,存储该顶点对应的下标
EdgeType weight; //存储权值,非网图不需要
struct EdgeNode *next;
}EdgeNode;
type struct VertexNode{ //顶点表结点
VertexType data; //顶点信息
EdgeNode *firstedge; //边表头指针
}VertexNode,AdjList[MAXSIZE];
type struct{
AdjList adjList;
int numVertexes,numEdges; //图中当前顶点数和边数
}GraphAdjList;
/*建立无向图图的邻接表结构,算法的复杂度为O(n+e)*/
void CreateGraphAL(GraphAdjList *G){
int i, j, k;
EdgeNode *e;
printf("请输入顶点数和边数(输入格式为:顶点数,边数):\n");
// 读入顶点数和边数
scanf("%d,%d", &(G->numVertexes), &(G->numEdges));
for (i = 0; i < G->numVertexes; i++){// 读入顶点信息,建立顶点表
scanf("\n%c", &(G->adjList[i].data)); // 读入顶点信息
G->adjList[i].firstedge = NULL; // 边表头指针设为空
}
for (k = 0; k < G->numEdges; k++){ // 建立边表
printf("请输入边的信息(输入格式为:i,j):\n");
scanf("%d,%d", &i, &j); // 读入边(Vi,Vj)的顶点对应序号
/*相当于将j的结点头插法插入到顶点i的边表链中*/
e=(EdgeNode *)malloc(sizeof(EdgeNode)); //生成新边表结点
e->adjvex = j; // 邻接点序号为j
// 将新边表结点s插入到顶点Vi的边表头部
e->next = G->adjList[i].firstedge;
G->adjList[i].firstedge = e;
/*相当于将i的结点头插法插入到顶点j的边表链中*/
e=(EdgeNode *)malloc(sizeof(EdgeNode));
e->adjvex = i;
e->next = G->adjList[j].firstedge;
G->adjList[j].firstedge = e;
}
}
在有向图中,邻接表是有缺陷的,关心了出度问题,要想知道入度,就必须遍历整个图,反之逆邻接表解决了入度却不能解决出度,那能否将邻接表与逆邻接表结合起来呢,答案是肯定的,于是就有了一种新的有向图的存储方法:十字链表法。
重新定义顶点表结构:firstin 表示入边表头指针,指向该顶点的入边表中的第一个结点;firstout表示出边表头指针,指向该顶点的出边表中的第一个结点。
data firstin firstout
重新定义边表结构:tailvex是指弧起点在顶点表的下标;headvex是指弧终点在顶点表中的下标;headlink是指入边表指针域,指向终点相同的下一条边;taillink是指出边表指针域,指向起点相同的下一条边。若是网,还可增加权值域来保存权值。
tailvex headvex headlink taillink
红线箭头表示该图的逆邻接表的表示,蓝色箭头表示该图的邻接表。对于 v0 来说,有两个顶点 v1 和 v2 的入边,所以, v0 的firstin指向顶点 v1 的边界点中headvex为0的结点,接着,由入边节点的headlink指向下一个入边顶点 v2 。
十字链表的好处是将邻接表和逆邻接表整合在一起,既能找到以 vi 为尾的弧,又能找到以 vi 为头的弧,很容易求得顶点的出度和入度。除了结构复杂些,创建图算法的时间复杂度和邻接表相同,所以在有向图应用中,十字链表是个不错的数据结构模型。
十字链表法师有向图的优化存储结构,对于无向图而言呢?若在无向图应用中,关注的重点是顶点,那邻接表不错,但是若更关注边的操作,例如对已访问的边做标记,删除一条边等操作,这就是说要找到这条边的两个边表结点进行操作,还是比较麻烦的。
因此,仿照十字链表方法,对边表结点的结构进行改造,可避免边操作要找到两个结点的问题。
重新定义边表结点结构:ivex 和 jvex 是与某条边依附的两个顶点在顶点表中的下标;ilink 指向依附顶点 ivex 的下一条边,jlink指向依附顶点 jvex 的下一条边。
ivex ilink jvex jlink
其实,首先,由于顶点 v0 的 ( v0 , v1 ) 边的邻边为 ( v0 , v3 ) 和 ( v0 , v2 ) ,所以第一行的0后面的ilink指向第四行的0,同理第四行0后面的jlink指向第五行的0。注意ilink指向的结点的jvex一定要和它本身的ivex的值相等,同理,其他的连线也可相继得出。
邻接多重链表与邻接表的区别仅在于同一条边在邻接表中用两个结点表示,而邻接多重链表中只有一个结点,这样对边的操作就简单很多了。例如,若要删除上图的 ( v0 , v2 ) ,只需要将第二行2后的指针和第四行0后面的指针设为空即可。基本操作和邻接链表相似。
边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素有一条边的起点下标(begin)、终点下标(end)和权(weight)组成。
边集数组更关注边的集合,查找一丁点的度需要扫描整个边数组,效率不高,因此,其更适合对边依次进行处理的操作。
begin end weight