前言
在线性表中,数据元素之间仅有线性关系,每个数据元素只有一个直接前驱和一个直接后继;
在树形结构中,数据元素之间有明显的层次关系,并且每一层中的数据元素可能和下一层的多个元素(子结点)相关,但只能和上一层的一个元素(父结点)相关;
在图结构中,结点之间的关系是任意的,可以说树形结构是特殊的图结构。
图(Graph)G由两个集合V和G组成,记作G = (V,G)。其中V是各顶点(结点)的有穷非空集合,V中的任意两个顶点配对后作为集合E的元素,顶点偶对亦称为边。
在有向图中,E中的元素形式为
在无向图中,E中的元素形式为(x,y),仅表示连接顶点x和顶点y的一条边,效果同(y,x)。
在实际应用中,每条边可以标上具有某种含义的数值,该数值称为边上的权,这些权可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图又称作网。
由于图的任意两个顶点之间都可能存在联系,因此无法以数据元素在存储区中的物理位置来表示元素之间的关系,即图没有顺序存储结构,但我们可以用二维数组(矩阵)来表示元素之间的关系——邻接矩阵。除此之外还有链式存储结构,包括邻接表、十字链表和邻接多重表。其中邻接矩阵和邻接表最常用。
邻接矩阵(Adjacency Matrix)是表示顶点之间相邻关系的矩阵,存储在二维数组中。
1)在无向图中,若顶点v1与v2有联系,v1与v3没有联系,则在矩阵中,(1,2)和(2,1)的值为1,(1,3)和(3,1)的值为0,对称矩阵;
2)在无向网中,若边(v1,v2)的权为7,v1与y3没有联系,则在矩阵中,(1,2)和(2,1)的值为7,(1,3)和(3,1)的值为∞(无限);
3)在有向图中,若v1为弧尾,v2为弧头,v2不指向v1,则在矩阵中,(1,2)的值为1,(2,1)的值为0;
4)有向网的情况可以类推。
*需要注意的是,矩阵和二维数组的“坐标”含义是不同的,矩阵的(1,1)等价于二维数组中的[0,0]。
邻接矩阵仅记录着任意两点间的联系,我们还需要一个一维数组来记录每个顶点的信息,有边有顶点,这样才能构成一个完整的图。
清楚邻接矩阵的使用方法后,我们可以轻松写出以它为基础的图的结构形式。
为了避免概念的混淆,之后我会把图称作邻接矩阵图(AMGraph)。
/*邻接矩阵的结构表示*/
#include
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的出度就是多少,多少个
2)缺点:
a. 不便于增加和删除顶点。非链式结构的通病。
b. 不便于统计边的数目,需要遍历邻接矩阵的所有元素,时间复杂度为O(n²)。无向图遍历完后还要除以2。
c. 空间复杂度高。非链式结构的另一通病,稀疏图(边或弧数较少)尤其浪费空间。
邻接表(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. 不便于计算有向图各个顶点的入度,需要遍历其余所有起始顶点的单链表。
1)由上面总结的各自的优缺点可以看出,“我的优点就是你的缺点,and vice versa”。
2)一个图的邻接矩阵唯一,而邻接表不唯一。前面提过,“图中有多少个顶点,就有多少个单链表,每个顶点分别作为每个单链表的头结点,与顶点相连的其余顶点无序地(无特殊的顺序要求)连接在对应的单链表上”,因此每个单链表的链接次序取决于算法以及输入。
3)邻接矩阵和邻接表是图最常用的两种存储结构。(所以“十字链表”和“邻接多重表”暂时就不详细介绍喽_(:з」∠)_)
4)在下一篇将会讲到的“遍历图”中,不论是深度优先搜索还是广度优先搜索,存储结构为邻接矩阵的时间复杂度都为O(n²),存储结构为邻接表的时间复杂度都为O(n+e)。
十字链表(Orthogonal List)是有向图的另一种链式存储结构,可以看作是有向图的邻接表(单链表的结点数-1为出度)与逆邻接表(单链表的结点数-1为入度)结合起来的一种链表。
在十字链表中容易找到以vi(vi∈G(V))为弧尾或弧头的弧,因而更容易求顶点的入度和出度。
邻接多重表(Adjacency Multilist)是无向图的另一种链式存储结构。
对无向图而言,邻接表的缺点:每一条边(va,vb)有两个结点,分别在第a和第b个单链表中,这可能会给边的搜索、删除等操作带来了不便。
邻接多重表与邻接表的区别仅在于,同一条边,在邻接表中要用两个结点表示,而在邻接多重表中只需要一个结点。
此外,邻接多重表中增加了标志域用以标记该条边是否被搜索过,避免了同一条边的重复搜索。
路过的圈毛君:“图形结构分三块讲:构造图、遍历图和应用图。本来想一次写完的,无奈精力有限,要做的事比较多.......而且我也差不多该开始护肝了_(:з」∠)_”