图的存储必须要完整、准确地反应顶点集和边集的信息。根据不同图的结构和算法,采用不同的存储方式将对程序的效率产生相当大的影响,因此所选的存储结构应适合于待求解的问题。下面介绍四种图的存储方式。
一、邻接矩阵法
1、概念:
邻接矩阵存储是指用一个一维数组存储图中顶点的信息,用一个二维数组存储图中边的信息(即各顶点之间的邻接关系),存储顶点之间邻接关系的二维数组称为邻接矩阵。
【在简单应用中,可直接用二维数组作为图的邻接矩阵(顶点信息等均可忽略)】
顶点数为n的图G=(V, E)的邻接矩阵A是n×n的,将G的顶点编号为v1,v2,…,vn,则:
对于带权图而言,若顶点vi和vj之间有边相连,则邻接矩阵中对应项存放着该边对应的权值,若顶点vi和vj不相连,则通常用0或∞来代表这两个顶点之间不存在边:
有向图、无向图和网对应的邻接矩阵示例如下图所示:
2、图的邻接矩阵存储结构定义:
#define MaxVertexNum 100//顶点数目的最大值
typedef char VertexType;//顶点对应的数据类型
typedef int EdgeType;//边对应的数据类型
typedef struct{
VertexType vex[MaxVertexNum];//顶点表
EdgeType edge[MaxVertexNum][MaxVertexNum];//邻接矩阵(边表)
int vexnum, arcnum;//图当前的顶点数和边数
}MGraph;
【注】邻接矩阵表示法的空间复杂度为O(n2),其中n为图的顶点数|V|。
3、图的邻接矩阵存储表示法的特点:
- 对于有n个顶点的无向图,有A[i][i]=0,其中1<=i<=n。
- 无向图的邻接矩阵一定是一个对称矩阵(且唯一),即A[i][j]=A[j][i](1<=i<=n,1<=j<=n),因此,在实际存储邻接矩阵时只需存储上(或下)三角矩阵的元素(压缩存储)。
- 对于无向图,邻接矩阵的第 i 行(或第 i 列)非零元素(或非∞元素)的个数正好是顶点 i 的度TD(vi)。
- 对于有向图,邻接矩阵的第 i 行非零元素(或非∞元素)的个数正好是顶点 i 的出度OD(vi);邻接矩阵的第 i 列非零元素(或非∞元素)的个数正好是顶点 i 的入度ID(vi)。
- 用邻接矩阵存储图,很容易确定图中任意两个顶点之间是否有边相连,但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大。
- 稠密图(即边数较多的图)适合采用邻接矩阵的存储方式。
- (了解即可)设图G的邻接矩阵为A,Am的元素Am [i][j]等于由顶点i到顶点j的长度为m的路径的数目。
4、例题:
C
D
D
B、D
C【2013年统考真题】
二、邻接表法
1、概念:
当一个图为稀疏图时,使用邻接矩阵法会浪费大量的存储空间,而图的邻接表法结合了顺序存储和链式存储的方法,大大减少了这种不必要的浪费。
邻接链表是指对图G中的每个顶点vi建立一个单链表,第i个单链表中的结点表示依附于顶点vi的边(对于有向图则是以顶点vi为尾的弧),这个单链表就称为顶点vi的边表(对于有向图则称为出边表)。边表的头指针和顶点的数据信息采用顺序存储,称为顶点表,所以在邻接表中存在两种结点:顶点表结点和边表结点,如下图所示:
顶点表结点由两个域组成:
- 顶点域(data)存储顶点vi的相关信息;
- 边表头指针域(firstarc)指向第一条边的边表结点。
边表结点至少由两个域组成:
- 邻接表域(adjvex)存储与头结点顶点vi邻接的顶点编号;
- 指针域(nextarc)指向下一条边的边表结点。
无向图、有向图对应的邻接表示例如下图所示:
2、图的邻接表存储结构定义:
#define MaxVertexNum 100//图中顶点数目的最大值
typedef struct ArcNode{//边表结点
int adjvex;//该弧所指向的顶点的位置
struct ArcNode *nextarc;//指向下一条弧的指针
//InfoType info;//网的边权值
}ArcNode;
typedef struct VNode{//顶点表结点
VertexType data;//顶点信息
ArcNode *firstarc;//指向第一条依附该顶点的弧的指针
}VNode,AdjList[MaxVertexNum];
typedef struct{
AdjList vertices;//邻接表
int vexnum, arcnum;//图的顶点数和弧数
}ALGraph;//ALGraph是以邻接表存储的图类型
【注】若G为无向图,则图的邻接表存储方法所需的存储空间为O(|V|+2|E|),若G为有向图,则所需的存储空间为O(|V|+|E|)。
3、图的邻接表存储表示法的特点:
- 对于稀疏图(即边数较少的图),采用邻接表表示将极大地节省存储空间。
- 在邻接表中,给定一个顶点,能很容易地找出它的所有邻边,因为只需要读取它的邻接表。在邻接矩阵中,相同的操作则需要扫描一行,花费的时间为O(n)。但是,若要确定给定的两个顶点间是否存在边,则在邻接矩阵中可以立刻查到,而在邻接表中则需要在相应结点对应的边表中查找另一结点,效率较低。
- 在无向表的邻接表中,求某个顶点的度只需计算其邻接表中的边表结点个数。在有向图的邻接表中,求某个顶点的出度只需计算其邻接表中的边表结点个数;但求某个顶点x的入度则需遍历全部的邻接表,统计邻接点(adjvex)域为x的边表结点个数。
- 图的邻接表表示并不唯一,因为在每个顶点对应的边表中,各边结点的链接次序可以是任意的,它取决于建立邻接表的算法及边的输入次序。
4、例题:
B、A、D、C、B、D
三、十字链表法
1、概念:
十字链表是有向图的一种链式存储结构。在十字链表中,有向图的每条弧用一个结点(弧结点)来表示,每个顶点也用一个结点(顶点结点)来表示。如下图所示:
弧结点中有5个域:
- tailvex域指示弧尾顶点的编号;
- headvex域指示弧头顶点的编号;
- 头链域hlink指向弧头相同的下一个弧结点;
- 尾链域tlink指向弧尾相同的下一个弧结点;
- info域存放该弧的相关信息。
这样,弧头相同的弧在同一个链表上,弧尾相同的弧也在同一个链表上。
顶点结点中有3个域:
- data域存放该顶点的数据信息,如顶点名称;
- firstin域指向以该顶点为弧头的第一个弧结点;
- firstout域指向以该顶点为弧尾的第一个弧结点。
2、有向图的十字链表表示示例:
注意:顶点结点之间是顺序存储的,弧结点省略了info域。
在十字链表中,既容易找到vi为尾的弧,也容易找到vi为头的弧,因而容易求得顶点的出度和入度。图的十字链表表示是不唯一的,但一个十字链表表示唯一确定一个图。下面来详细分析一下上面的示例:
十字链表适用于有向图,它实际上是邻接表和逆邻接表的结合。邻接表的单链表指向的是顶点的出边表,与之相反,逆邻接表的单链表指向的是顶点的入边表。
我们先来看顶点的出边表部分:(进行水平方向的连接)
1)v1指向v2、v3:
2)v3指向v1、v4
v4指向v1、v2、v3:
再来看顶点的入边表部分:(进行垂直方向的连接)
3)v1被v3、v4指向:
4)v2被v1、v4指向
v3被v1、v4指向
v4被v3指向:
再举一个例子:(自己练习一下吧)
3、例题:
B
四、邻接多重表法
1、概念:
邻接多重表是无向图的一种链式存储结构。在邻接表中,容易求得顶点和边的各种信息,但在邻接表中求两个顶点之间是否存在边而对边执行删除等操作时,需要分别在两个顶点的边表中遍历,效率较低。与十字链表相似,在邻接多重表中,每个顶点、每条边用一个结点表示,其结构如下所示:
边结点中有5个域:
- ivex域指示该边依附的其中一个顶点的编号;
- jvex域指示该边依附的另一个顶点的编号;
- link域指向下一条依附于顶点ivex的边;
- jink域指向下一条依附于顶点jvex的边;
- info域存放该边的相关信息。
顶点结点中有2个域:
- data域存放该顶点的相关信息;
- firstedge域指向第一条依附于该顶点的边。
在邻接多重表中,所有依附于同一顶点的边串联在同一链表中,因为每条边依附于两个顶点,所以每个边结点同时链接在两个链表中。对无向图而言,其邻接多重表和邻接表的差别仅在于:同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。
2、无向图的邻接多重表表示示例:
注意:顶点结点之间是顺序存储的,弧结点省略了info域,邻接多重表的各种基本操作的实现和邻接表类似。下面来详细分析一下上面的示例:
1)图中顶点b和顶点c所连的边(3条)最多,因此先画顶点b的边表:
b-a(1-0)、b-c(1-2)、b-e(1-4)
2)除去三条边后,顶点c和顶点d所连的边(2条)最多,由于间隔一个顶点画出的图更好看,因此画顶点d的边表:
d-a(3-0)、d-c(3-2)
3)最后只剩e-c(4-2)这条边了,把它加入顶点e的边表中:
4)连线,规则是与编号相同的就串联起来,比如顶点a的编号为0,那么找到边表中有含0的结点,依次串联起来即可:
可以看出邻接多重表的表示方式不唯一。
再举两个例子:(自己练习一下吧)
3、例题:
A
4、图的四种存储方式的总结: