数据结构与算法(图论系列)------邻接矩阵与邻接表详解
图图的定义图(Graph)G由两个集合V和G组成,记作G = (V,G)。其中V是各顶点(结点)的有穷非空集合,V中的任意两个顶点配对后作为集合E的元素,顶点偶对亦称为边。在有向图中,E中的元素形式为<x,y>,表示从顶点x到顶点y的一条有向边,有向边也称作弧,x为弧尾,y为弧头;在无向图中,E中的元素形式为(x,y),仅表示连接顶点x和顶点y的一条边,效果同(y,x)。在实际应用中,
图
图的定义
图(Graph)G由两个集合V和G组成,记作G = (V,G)。其中V是各顶点(结点)的有穷非空集合,V中的任意两个顶点配对后作为集合E的元素,顶点偶对亦称为边。
- 在有向图中,E中的元素形式为<x,y>,表示从顶点x到顶点y的一条有向边,有向边也称作弧,x为弧尾,y为弧头;
- 在无向图中,E中的元素形式为(x,y),仅表示连接顶点x和顶点y的一条边,效果同(y,x)。
在实际应用中,每条边可以标上具有某种含义的数值,该数值称为边上的权,这些权可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图又称作网。
图的存储结构
由于图的任意两个顶点之间都可能存在联系,因此无法以数据元素在存储区中的物理位置来表示元素之间的关系,即图没有顺序存储结构,但我们可以用二维数组(矩阵)来表示元素之间的关系——邻接矩阵。除此之外还有链式存储结构,包括邻接表、十字链表和邻接多重表。其中邻接矩阵和邻接表最常用。
邻接矩阵
定义和性质
图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(邻接矩阵)存储图中的边或弧的信息。
设图G有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:
看一个实例,下图左就是一个无向图:
从上面可以看出,无向图的边数组是一个对称矩阵。所谓对称矩阵就是n阶矩阵的元满足aij = aji。即从矩阵的左上角到右下角的主对角线为轴,右上角的元和左下角相对应的元全都是相等的。
从这个矩阵中,很容易知道图中的信息:
(1)要判断任意两顶点是否有边无边就很容易了,判断邻接矩阵中元素值a[i][j]是0是1即可;
(2)要知道某个顶点的度,其实就是 这个顶点vi在邻接矩阵中第i行或(第i列)的元素之和;
(3)求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1就是邻接点;
而有向图讲究入度和出度,顶点vi的入度为1,正好是第i列各数之和。顶点vi的出度为2,即第i行的各数之和。
若图G是网图,有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:
代码实现
清楚邻接矩阵的使用方法后,我们可以轻松写出以它为基础的图的结构形式。
为了避免概念的混淆,之后我会把图称作邻接矩阵图(AMGraph)。
/*邻接矩阵的结构表示*/
#include <iostream>
using namespace std;
#define MaxInt 32767 /*表示∞(无限)*/
#define MaxVNum 100 /*表示图中最多可以包含的顶点数*/
typedef char Vextype; /*将顶点的数据类型设为字符型,如果你喜欢也可以把它设成更复杂的结构体*/
typedef int Arctype; /*将边的权值设为整型*/
typedef struct
{
Vextype vexs[MaxVNum]; /*用来保存顶点信息的一维数组*/
Arctype arcs[MaxVNum][MaxVNum]; /*邻接矩阵*/
int vexnum, arenum; /*记录图的顶点数和边数*/
} AMGraph; /*将结构体命名为AMGraph*/
写出邻接矩阵图的结构形式后,可以开始往里面放东西了。
下面以“无向网”为例写一下邻接矩阵表示法的使用方法:
<思路>
(1)数一下无向网有多少个顶点(vexnum),多少条边(arcnum),然后将数值输入。当然,你也可以凭空造一个网,在先前定义的MaxVNum内赋值就行。
(2)在一维数组vex内输入顶点信息,一个循环完成。
(3)初始化邻接矩阵,将每个权值初始化为MaxInt。为什么呢?因为矩阵里除了若干个权值外,剩余的都是MaxInt,不可能先把权值填好,再来一个个把没有权值的地方填上MaxInt,这样效率极低而且繁琐。
(4)构造邻接矩阵。依次输入每条边依附的两个顶点v1和v2以及其权值w并赋值。因为输入的v1、v2有可能不在0~MaxInt的范围内,而且矩阵和二维数组的“坐标”含义是不同的,即矩阵的(1,1)等价于二维数组中的[0,0],所以需要一个函数LocateVex()帮助判断、转化并确定“坐标”。
int CreatUDN(AMGraph &G) /*UDN意为Omnidirectional Net,无向网*/
{ /*上面<思路>中写了的东西下面就不再啰嗦喽*/
int i, j, w;
cin>>G.vexnum>>G.arcnum;
for(i = 0; i < G.vexnum; i++) cin>>G.vexs[i];
for(i = 0; i < G.vexnum; i++)
for(j = 0; j < G.vexnum; j++)
G.arcs[i][j] = MaxInt;
for(int k = 0; k < G.arcnum; k++)
{
cin>>v1>>v2>>w;
i = LocateVex(G, v1); j = LocateVex(G, v2);
G.arc[i][j] = G.arc[j][i] = w;
}
return 0;
}
优缺点
1)优点:
a. 便于判断两个顶点是否有联系。确定顶点后再确定矩阵上的相应位置是否非0或非MaxInt即可。
b.便于计算各个顶点的度。其实也不用计算,数就完事了。对于无向图,多少个(1,n)的值为1,v1的度就是多少;对于有向图,多少个<1,n>的值为1,v1的出度就是多少,多少个<n,1>的值为1,v1的入度就是多少。
2)缺点:
a. 不便于增加和删除顶点。非链式结构的通病。
b. 不便于统计边的数目,需要遍历邻接矩阵的所有元素,时间复杂度为O(n²)。无向图遍历完后还要除以2。
c. 空间复杂度高。非链式结构的另一通病,稀疏图(边或弧数较少)尤其浪费空间。
邻接表
定义和性质
邻接矩阵是不错的一种图存储结构,但是,对于边数相对顶点较少的图,这种结构存在对存储空间的极大浪费。因此,找到一种数组与链表相结合的存储方法称为邻接表。
邻接表的处理方法是这样的:
(1)图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过,数组可以较容易的读取顶点的信息,更加方便。
(2)图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以,用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表。
例如,下图就是一个无向图的邻接表的结构:
从图中可以看出,顶点表的各个结点由data和firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点。边表结点由adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针。
对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可。如下图所示。
代码实现
邻接表(Adjacency List)是图的一种链式结构。图中有多少个顶点,就有多少个单链表,每个顶点分别作为每个单链表的头结点,与顶点相连的其余顶点无序地(无特殊的顺序要求)连接在对应的单链表上。
顶点间的关系蕴含在若干个单链表中,因此邻接表图(ALGraph)的结构体只需要包含{每个单链表的头结点(即每个顶点) 以及 顶点数和边数}。
为了方便管理头结点,我们可以把它们放到一维数组ALs[MaxVNum]中,且该一维数组的功能包含着邻接矩阵图中vexs[MaxVNum]的功能。
*下面将头结点代表的顶点称作起始顶点。
再把注意力放到单链表上,由于起始顶点已经确定,所以与起始顶点相连的每一个顶点都分别对应着一条边,因此我们可以把单链表上的其余结点看作边结点。
边结点需要包含的信息有{边依附的另一个顶点编号(因为顶点放在一维数组ALs[MaxVNum]中,所以顶点v1的编号为0,如此类推)、指向下一个边结点的指针 以及 边上的权}。
头结点需要包含的信息有{起始顶点的信息、指向下一个边结点的指针}
依据上面的思路,我们一共需要创建三个结构体。
/*邻接表的结构表示*/
typedef struct ArcNode /*边结点*/
{
int anothervex; /*另一个顶点的编号*/
ArcNode *nextarcnode; /*指向下一边结点的指针*/
int weight; /*权*/
};
typedef struct VexNode /*头结点*/
{
VexType data; /*起始顶点的信息*/
ArcNode *nextarcnode; /*指向下一边结点的指针*/
};
typedef struct ALGraph /*邻接表图*/
{
VexNode ALs[MaxInt]; /*头结点数组*/
int vexnum, arcnum; /*顶点数、边数*/
};
写出邻接表图的结构形式后,可以开始往里面放东西了。
下面以“无向图”为例写一下邻接表表示法的使用方法:
<思路>
(1)输入无向图的顶点数和边数;
(2)在一维数组ALs[MaxVNum]中输入起始顶点的信息,并使指针域初始化为NULL,完成各单链表的初始化;
(3)构造邻接表。输入起始顶点va和相邻顶点va的值,以及(va,vb)的权w。
(4)new一个临时边结点tempnode,使tempnode中“另一顶点的编号”为vb的编号,并使其指针指向头结点指针原本指向的“空间”,再令头结点指针指向tempnode,完成tempnode的插入,即完成相邻顶点的连接。
(5)因为这个是无向图,所以我们需要对称地完成起始顶点为vb的单链表的插入。
(6)在达到边数arcnum前循环执行步骤(3)(4)(5)。
int CreatUDG(ALGraph &G)
{ /*下面的代码已经按照<思路>步骤分块喽*/
cin>>G.vexnum>>G.arcnum;
int i, j;
for(i =0; i < G.vexnum; i++)
{
cin>>G.ALs[i].data;
G.ALs[i].nextarcnode = NULL;
}
for(int k = 0; k < G.arcnum; k++)
{
cin>>va>>vb>>w;
i = LocateVex(G, va); j = LocateVex(G, vb);
tempnode1 = new ArcNode;
tempnode1->anothervex = j;
tempnode1->nextarcnode = G.ALs[i].nextarcnode;
G.ALs[i].nextarcnode = tempnode1;
tempnode2 = new ArcNode;
tempnode2->anothervex = i;
tempnode2->nextarcnode = G.ALs[j].nextarcnode;
G.ALs[j].nextarcnode = tempnode2;
}
return 0;
}
优缺点
1)优点:
a. 便于增加和删除顶点。
b. 便于统计边的数量,时间复杂度为O(n+e)。
c. 空间效率高。
2)缺点:
a. 不便于判断顶点间是否有联系。
b. 不便于计算有向图各个顶点的入度,需要遍历其余所有起始顶点的单链表。
总结
- 对于一个具有n个顶点e条边的无向图,它的邻接表表示有n个顶点表结点2e个边表结点
- 对于一个具有n个顶点e条边的有向图,它的邻接表表示有n个顶点表结点e个边表结点
- 如果图中边的数目远远小于n2称作稀疏图,这是用邻接表表示比用邻接矩阵表示节省空间;
- 如果图中边的数目接近于n2,对于无向图接近于n*(n-1)称作稠密图,考虑到邻接表中要附加链域,采用邻接矩阵表示法为宜。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)