何为邻接表?
由于邻接矩阵存储图的方式,需要固定的开辟一块空间,比如顶点多,但是边很少,这样的邻接矩阵中存在大量的未利用空间。同时,使用数组的方式,虽然查询遍历方便,但是有时需要增删操作时,会比较麻烦且耗时。于是乎,引申出了邻接表,采用数组+单链表的方式实现链式存储。相对灵活了很多。数组存储顶点,单链表存储相应顶点相关的边集。
如何实现?
根据这样的定义,我们可以先定义头结点:
data | firstarc |
---|
并以头结点为基本元素定义一个数组。
接着定义表结点:
adjvex | nextarc |
---|
然后将顶点信息存入数组data域,或者直接下标间接表示顶点编号。如果某顶点存在关联的边,则申请一个表结点类型变量存储信息,将表结点(表示边信息)插入到对应顶点的边表中即可。这里需要考虑插入的操作如何实现,可以插在头部中间尾部,但是肯定直接插在头部,也就是头结点之后原有第一个表结点前最为方便,这样的时间复杂度为O(1)。
其实顺序需要调整一下,应该先定义表结点,因为头结点的firstarc是一个指向表结点类型的指针。
开始实现
这样的数组+单链表,和哈希表有点类似,下面借用了哈希表中的描述,称数组元素为盒子。
#define MAXVEX 20 //最大顶点数
//定义边表结点
typedef struct _ArcNode
{
int adjvex;//当前数组盒子指向顶点的位置
struct _ArcNode * nextarc;//指向当前数组盒子下一条边的指针
}ArcNode;
//定义表头结点
typedef struct _VNode
{
int data;//可以利用这个data域存储某个数组盒子的边表长度以指示边数量
ArcNode * firstarc;//指向这个盒子的第一条边
}VNode;
//定义图
typedef struct _Graph
{
int vexnum,arcnum;//顶点数目,边数目
VNode vexs[MAXVEX];
}Graph;
这样便完成了图的邻接表实现的相关定义。接着实现初始化的操作以及写入边信息。
//将顶点数组创建完成,并存储汇总信息
void CreateGraph(Graph * G, int vexnum, int arcnum)
{
G->vexnum = vexnum;
G->arcnum = arcnum;
for (int i = 0; i < MAXVEX; ++i)
{
(G->vexs[i]).firstarc = NULL;
(G->vexs[i]).data = 0;
}
}
//定义一个边结构,方便插入操作
typedef struct _Edge
{
int v1,v2;//v1出发点编号,v2终点编号,可依据有向或是无向具体化
WeightType weight;//边权值,可选
}Edge;
//将边信息以边表结点的形式插入到对应的盒子
void InsertEdge(Graph * G, Edge E)
{
//申请边表结点空间
ArcNode * arc = (ArcNode *) malloc (sizeof(ArcNode));
arc->adjvex = E->v2;
arc->nextarc = NULL;
//插入到边表中
if((G->vexs[E->v1]).data == 0)
(G->vexs[E->v1]).firstarc = arc;
else{
arc->nextarc = (G->vexs[E->v1]).firstarc;
(G->vexs[E->v1]).firstarc = arc;
}
//成功插入,对应盒子的边表结点数量自增
(G->vexs[E->v1]).data += 1;
}
总结
- 上述定义没有区分有向图或是无向图,在存储具体的图结构时,相应的链式结构需要做一些改变以适应相应的需求。
- 比如,这样的邻接表,存储无向图时,我们重复存储了同一边信息,也就是用了两个边结点表示一条边,其实是完全没必要的。针对无向图,这一点作出优化,于是产生有邻接多重表,避免重复存边信息。
- 存储有向图时,由于这样的表结构边表其实只是对应顶点出度的弧表,要知道对应顶点的入度,则需要遍历整个数组盒子+边表,才能确定是否存在其他顶点指向这个顶点,也就是判断入度的过程,这样是非常麻烦的。针对有向图,这一点作出优化,于是产生有十字链表,方便查询顶点入度及出度。
- 其实有时,这样的链式存储也未必节省了空间,在边表结点较多的情况下,比如稠密图,数组中和边表中增加的指针域与存储的信息量可比,需要V个表头结点,2E个表结点,相当于花了两倍多的空间存储。但是在稀疏图中边相对较少的情况,从节省空间的角度上来说,使用邻接表表示会较优。