知识框架
图的基本概念
在线性表中,数据元素之间是被串起来的,仅有线性关系,每个数据元素只有一个直接前驱和一个直接后继。在树形结构中,数据元素之间有着明显的层次关系,并且每一层上的数据元素可能和下一层中多个元素相关,但只能和上一层中一个元素相关。图是一种较线性表和树更加复杂的数据结构。在图形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。
一、图的定义
图(Graph)是由顶点的有穷非空集合V(G)和顶点之间边的集合E(G)组成,通常表示为: G=(V,E),其中,G表示个图,V是图G中顶点的集合,E是图G中边的集合。若V={v1,v2,...,vn} ,则用∣V∣表示图G中顶点的个数,也称图G的阶,E={(u,v)∣u∈V,v∈V},用∣E∣表示图G中边的条数。
注意:线性表可以是空表,树可以是空树,但图不可以是空图。就是说,图中不能一个顶点也没有,图的顶点集V一定非空,但边集E可以为空,此时图中只有顶点而没有边。
图的基本概念和术语
有向图
若E是有向边(也称弧)的有限集合时,则图G为有向图。弧是顶点的有序对,记为<v, w>,其中v,w是顶点,v称为弧尾,w称为弧头,<v,w>称为从顶点v到顶点w的弧,也称v邻接到w,或w邻接自v。
图(a)所示的有向图G1可表示为
G1=(V1,E1)
V1={1,2,3}
E1={<1,2>,<2,1>,<2,3>}
有向图:每条边都是有方向的
无向图:每条边都是无方向的
完全图:任意两个点都有一条边相连
稀疏图:有很少边或弧的图
稠密图:有较多边或弧的图
网:边/弧相连的两个顶点之间的关系 存在(vi,vj),则称vi和vj互为邻接点;存在<vi,vj>,则称vi邻接到vj,vj邻接于vi
关联(依附):边/弧与顶点之间的关系。 存在(vi,vj)/<vi,vj>,则称该边/弧关联于vi和vj
顶点的度:与该顶点相关联的边的数目,记为TD(v)
在有向图中,顶点的度等于该顶点的入度与出度之和
v的入度:是以v为终点的有向边的条数,记作ID(v)
v的出度:是以v为始点的有向边的条数,记作OD(v)
路径: 接续的边构成的顶点序列
路径长度:路径上边或弧的数目/权值之和
回路(环):第一个顶点和最后一个顶点相同的路径
简单路径:除路径起点和终点可以相同外,其余顶点均不相同的路径
简单回路(简单环): 除路径起点和终点相同外,其余顶点均不相同的路径
连通图(强连通图):在无(有)向图G=(V,{E})中,若对任何两个顶点v,u 都存在从v到u的路径,则称G是连通图(强连通图)
权与网:图中边弧所具有的相关数称为权,表明从一个顶点到另一个顶点的距离或消耗。带权的图称为网
子图:设有两个图G=(V,{E}),G1=(V1,{E1}),若V1是V的子集,且E1是E的子集,则称G1是G的子图。
连通分量(强连通分量):无向图G的极大连通子图称为G的连通分量,极大连通子图的意思是该子图是G连通子图,将G的任何不在该子图的顶点加入,子图不再连通
极小连通子图:该子图是G的连通子图,在该子图中删除任何一条边子图不再连通
生成树:包含无向图G所有顶点的极小连通子图
生成森林:对非连通图,由各个连通分量的生成树的集合
邻接矩阵
图的邻接矩阵(Adjacency Matrix) 存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
设图G有n个顶点,则邻接矩阵A AA是一个n∗n的方阵,定义为:
图是一个无向图和它的邻接矩阵:
1.无向图的邻接矩阵一定是一个对称矩阵(即从矩阵的左上角到右下角的主对角线为轴,右上角的元与左下角相对应的元全都是相等的)。 因此,在实际存储邻接矩阵时只需存储上(或下)三角矩阵的元素。
2.对于无向图,邻接矩阵的第i行(或第i列)非零元素(或非∞元素)的个数正好是第i个顶点的度TD(vi)。比如顶点v1的度就是1+0+1+0=2。
求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,A[i][j]为 1就是邻接点。
下图是有向图和它的邻接矩阵:
1.主对角线上数值依然为0。但因为是有向图,所以此矩阵并不对称。
2.有向图讲究入度与出度,顶点v1的入度为1,正好是第v1列各数之和。顶点v1的出度为2,即第v1行的各数之和。
3.与无向图同样的办法,判断顶点vi到vj是否存在弧,只需要查找矩阵中A[i][j] 是否为1即可。
对于带权图而言,若顶点vi和vj之间有边相连,则邻接矩阵中对应项存放着该边对应的权值
下图是有向网图和它的邻接矩阵:
通过以上对无向图、有向图和网的描述,可定义出邻接矩阵的存储结构:
邻接矩阵存储
#define MaxVertexNum 100 //顶点数目的最大值
typedef char VertexType; //顶点的数据类型
typedef int EdgeType; //带权图中边上权值的数据类型
typedef struct{
VertexType Vex[MaxVertexNum]; //顶点表
EdgeType Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexnum, arcnum; //图的当前顶点数和弧树
}MGraph;
注意:
①在简单应用中,可直接用二维数组作为图的邻接矩阵(顶点信息等均可省略)。
②当邻接矩阵中的元素仅表示相应的边是否存在时,EdgeType可定义为值为0和1的枚举类型。
③无向图的邻接矩阵是对称矩阵,对规模特大的邻接矩阵可采用压缩存储。
④邻接矩阵表示法的空间复杂度为O(n2), 其中n为图的顶点数∣V∣。
⑤ 用邻接矩阵法存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大。
⑥ 稠密图适合使用邻接矩阵的存储表示。
采用邻接矩阵表示法创建无向图(有向图)
算法思想:
1.输入总顶点数和总边数
2.依次输入点的信息存入顶点表中
3.初始化邻接矩阵,使每个权值初始化为极大值
4.构造邻接矩阵
Status CreateUDN(AMGraph &G){//采用邻接矩阵表示法,创建无向网G
cout<<"\n请输入顶点个数:";
cin>>G.vexnum;
cout<<"请输入边的个数:";
cin>>G.arcnum;
cout<<"请输入顶点名称:"<<endl;//输入总顶点数,总边数
for(int i=0;i<G.vexnum;i++) cin>>G.vexs[i];//依次输入点的信息
for(int i=0;i<G.vexnum;i++){//初始化邻接矩阵
for(int j=0;j<G.vexnum;j++){
G.arcs[i][j]=MaxInt;//边的权值均置为极大值
}
}
cout<<"请输入边连接的顶点:"<<endl;
for(int k=0;k<G.arcnum;k++){//构造邻接矩阵
cin>>v1>>v2>>W;//输入一条边所依附的顶点及边的权值
i=LocateVex(G,v1);
j=LocateVex(G,v2);//确定v1v2在G中的位置
G.arcs[i][j]=w;//边<v1,v2>的权值置为w
G.arcs[j][i]=G.arcs[i][j];//置<v1,v2>的对称边<v2,v1>的权值为w (这句话在建立有向图时注释掉)
}
return OK;
}
邻接表
当一个图为稀疏图时(边数相对顶点较少),使用邻接矩阵法显然要浪费大量的存储空间,如下图所示:
而图的邻接表法结合了顺序存储和链式存储方法,大大减少了这种不必要的浪费。
所谓邻接表,是指对图G中的每个顶点vi建立一个单链表,第i个单链表中的结点表示依附于顶点vi的边(对于有向图则是以顶点vi为尾的弧),这个单链表就称为顶点vi的边表(对于有向图则称为出边表)。边表的头指针和顶点的数据信息采用顺序存储(称为顶点表),所以在邻接表中存在两种结点:顶点表结点和边表结点,如下图所示。
顶点表结点由顶点域(data)和指向第一条邻接边的指针(firstarc) 构成,边表(邻接表)结点由邻接点域(adjvex)和指向下一条邻接边的指针域(nextarc) 构成。
无向图的邻接表的实例如下图所示。
顶点表结点由顶点域(data)和指向第一条邻接边的指针(firstarc) 构成,边表(邻接表)结点由邻接点域(adjvex)和指向下一条邻接边的指针域(nextarc) 构成。
无向图的邻接表的实例如下图所示。
对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可。
图的邻接表存储方法具有以下特点
若G为无向图,则所需的存储空间为O(∣V∣+2∣E∣);若G为有向图,则所需的存储空间O(∣V∣+∣E∣)。前者的倍数2是由于无向图中,每条边在邻接表中出现了两次
对于稀疏图,采用邻接表表示将极大地节省存储空间。
在邻接表中,给定一顶点,能很容易地找出它的所有邻边,因为只需要读取它的邻接表。在邻接矩阵中,相同的操作则需要扫描一行,花费的时间为O(n)。但是,若要确定给定的两个顶点间是否存在边,则在邻接矩阵中可以立刻查到,而在邻接表中则需要在相应结点对应的边表中查找另一结点,效率较低。
在有向图的邻接表表示中,求一个给定顶点的出度只需计算其邻接表中的结点个数;但求其顶点的入度则需要遍历全部的邻接表。因此,也有人采用逆邻接表的存储方式来加速求解给定顶点的入度。当然,这实际上与邻接表存储方式是类似的。
图的邻接表表示并不唯一,因为在每个顶点对应的单链表中,各边结点的链接次序可以是任意的,它取决于建立邻接表的算法及边的输入次序。
邻接表储存
#define MAXVEX 100 //图中顶点数目的最大值
type char VertexType; //顶点类型应由用户定义
typedef int EdgeType; //边上的权值类型应由用户定义
/*边表结点*/
typedef struct EdgeNode{
int adjvex; //该弧所指向的顶点的下标或者位置
//EdgeType weight; //权值,对于非网图可以不需要
struct EdgeNode *next; //指向下一个邻接点
//OtherInfo info; //和边相关的信息
}EdgeNode;
/*顶点表结点*/
typedef struct VertexNode{
Vertex data; //顶点域,存储顶点信息
EdgeNode *firstedge //边表头指针
}VertexNode, AdjList[MAXVEX];//AdjList表示邻接表类型
/*邻接表*/
typedef struct{
AdjList vertices;//vertices为vertex的复数
int numVertexes, numEdges; //图中当前顶点数和边数
}ALGraph;
邻接表建立无向图
Status CreateUDG(ALGraph &G)
{
cout<<"\n请输入顶点个数:";
cin>>G.vexnum;
cout<<"请输入边的个数:";
cin>>G.arcnum;/输入总顶点,总边数
cout<<"请输入顶点名称:"<<endl;/
for (int i=0;i<G.vexnum;i++)//输入各点,构造表头结点表
{
cin>>G.vertices[i].data;//输入顶点值
G.vertices[i].firstarc=NULL;//初始化表头结点的指针域
}
cout<<"请输入边连接的顶点:"<<endl;
for (int k=0;k<G.arcnum;k++)//输入各条边,构造邻接表
{
VerTexType v1,v2;
cin>>v1>>v2;//输入一条边依附的两个顶点
int i=LocateVexALG(G,v1);
int j=LocateVexALG(G,v2);
ArcNode *p1,*p2;
p1=new ArcNode;//生成一个新的边结点*p1
p1->adjvex=j;//邻接点序号为j
p1->nextarc=G.vertices[i].firstarc;
G.vertices[i].firstarc=p1;//将新结点*p1插入顶点vi的边表头部
p2=new ArcNode;//生成另一个对称的新的边结点*p2
p2->adjvex=i;//邻接点序号为i
p2->nextarc=G.vertices[j].firstarc;
G.vertices[j].firstarc=p2;//将新结点*p2插入顶点vj的边表头部
//建立有向网不用最后四部
}
return OK;
}
十字链表(主要用于有向图)
缺点:求结点的度难
十字链表(Orthogonal List)。
我们重新定义顶点表结点结构如下表所示。
其中firstin表示入边表头指针,指向该顶点的入边表中第一个结点,firstout 表示出边表头指针,指向该顶点的出边表中的第一个结点。
重新定义的边表结点结构如下表所示。
其中tailvex 是指弧起点在顶点表的下标,headvex 是指弧终点在顶点表中的下标,headlink是指入边表指针域,指向终点相同的下一条边,taillink是指边表指针域,指向起点相同的下一条边。如果是网,还可以再增加一个weight域来存储权值。
如下图所示,顶点依然是存入一个一维数组{V0,V1,V2,V3},实线箭头指针的图示完全与的邻接表的结构相同。就以顶点V0来说,firstout 指向的是出边表中的第一个结点V3。所以V0边表结点的headvex=3,而tailvex就是当前顶点V0的下标0,由于V0只有一个出边顶点,所以headlink和taillink都是空。
我们重点需要来解释虚线箭头的含义,它其实就是此图的逆邻接表的表示。对于V0来说,它有两个顶点V1和V2的入边。因此V0的firstin指向顶点V1的边表结点中headvex为0的结点,如上图右图中的①。接着由入边结点的headlink指向下一个入边顶点V2,如图中的②。对于顶点V1,它有一个入边顶点V2,所以它的firstin指向顶点V2的边表结点中headvex为1的结点,如图中的③。顶点V2和V3也是同样有一个入边顶点,如图中④和⑤。
十字链表的好处就是因为把邻接表和逆邻接表整合在了一起, 这样既容易找到以V1为尾的弧,也容易找到以V1为头的弧,因而容易求得顶点的出度和入度。而且它除了结构复杂一点外,其实创建图算法的时间复杂度是和邻接表相同的,因此,在有向图的应用中,十字链表是非常好的数据结构模型。
邻接多重表(主要用于无向图)
缺点:每条边都要储存两边
邻接多重表是无向图的另一种链式存储结构。
在邻接表中,容易求得顶点和边的各种信息,但在邻接表中求两个顶点之间是否存在边而对边执行删除等操作时,需要分别在两个顶点的边表中遍历,效率较低。比如下图中,若要删除左图的(V0,V2)这条边,需要对邻接表结构中右边表的阴影两个结点进行删除操作,显然这是比较烦琐的。
重新定义的边表结点结构如下表所示。
其中ivex和jvex是与某条边依附的两个顶点在顶点表中下标。ilink 指向依附顶点ivex的下一条边,jlink 指向依附顶点jvex的下一条边。这就是邻接多重表结构。
每个顶点也用一一个结点表示,它由如下所示的两个域组成。
其中,data 域存储该顶点的相关信息,firstedge 域指示第一条依附于该顶点的边。
我们来看结构示意图的绘制过程,理解了它是如何连线的,也就理解邻接多重表构造原理了。如下图7所示,左图告诉我们它有4个顶点和5条边,显然,我们就应该先将4个顶点和5条边的边表结点画出来。
我们开始连线,如图,首先连线的①②③④就是将顶点的firstedge指向一条边,顶点下标要与ivex的值相同,这很好理解。接着,由于顶点V0的(V0,V1)边的邻边有(V0,V3)和(V0,V2)。 因此⑤⑥的连线就是满足指向下一条依附于顶点V0的边的目标,注意ilink指向的结点的jvex一定要和它本身的ivex的值相同。同样的道理,连线⑦就是指(V1,V0)这条边,它是相当于顶点V1指向(V1,V2)边后的下一条。V2有三条边依附,所以在③之后就有了⑧⑨。连线④的就是顶点V3在连线④之后的下一条边。 左图一共有5条边,所以右图有10条连线,完全符合预期。
到这里,可以明显的看出,邻接多重表与邻接表的差别,仅仅是在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。 这样对边的操作就方便多了,若要删除左图的(V0,V2)这条边,只需要将右图的⑥⑨的链接指向改为NULL即可。