目录
一. 一些基本概念
图:多对多的结构关系。G=(V,E),偶对;
其中:V:顶点(数据元素)的有穷非空集合;E:边的有穷集合。
无向图:每条边都是无方向的;有向图:每条边都是有方向的
完全图:任意两个点都有一条边相连。对于无向完全图,n个顶点应该有n(n-1)/2条边,对于有向完全图,两个顶点之间两个方向的边都存在才能算是完全图。n个顶点应该有n(n-1)条边。
稀疏图:有很少边或弧(有向边)的图(e<nlogn)。稠密图:有较多边或弧的图。
网:边/弧带权的图。
邻接:有边/弧相连的两个顶点之间的关系,相连时称为这两个点是邻接的。
存在,则称
互为邻接点;若存在
,表示这条弧从
指向
,则称
邻接到
,
邻接于
;
(在离散数学中,()表示无序对,<>表示有序对,也称序偶)
关联(依附):边/弧与顶点之间的关系。若存在/
,则称该边/弧关联于
和
。
顶点的度:与该顶点相关联的边的数目,记为TD(v)
在有向图中,顶点的度等于该顶点的入度与出度之和。顶点v的入度是以v为终点的有向边的条数,记作ID(v);顶点v的出度是以v为始点的有向边的条数,记作OD(v);
当有向图中仅1个顶点的入度为0,其余顶点的入度均为1,此时是树,并且是有向树。
路径:接续的边构成的顶点序列。路径长度:路径上边或弧的数目/权值之和。
回路(环):第一个顶点和最后一个顶点相同的路径。
简单路径:除路径起点和终点可以相同外,其余顶点均不相同的路径。
简单回路(简单环):除路径起点和终点相同外,其余顶点均不相同的路径。
连通图(强连通图):在无(有)向图G=(V,{E})中,若对任何两个顶点v、u都存在从v到u的路径,则称G是连通图(强连通图)。
权与网:图中边或弧所具有的相关数称为权。表明从一个顶点到另一个顶点的距离或耗费。带权的图称为网。
子图:设有两个图G=(V,{E})、G1=(V1,{E1}),若,
,则称G1是G的子图。
连通分量(强连通分量):无(有)向图G的极大连通子图称为G的(强)连通分量。
极大连通子图:该子图是G连通子图,将G的任何不在该子图中的顶点加入,子图不再连通。
极小连通子图:该子图是G的连通子图,在该子图中删除任何一条边,子图不再连通。
生成树:包含无向图G所有顶点的极小连通子图。
生成森林:对非连通图,由各个连通分量的生成树的集合。
二. 图的抽象数据类型定义
ADT Graph{
数据对象V:具有相同特性的数据元素的集合,称为顶点集。
数据关系R:R={VR}
VR = {<v,w>|<v,w>|v,w∈V ^ p(v,w),
<v,w>表示从v到w的弧,p(v,w)定义了弧<v,w>的信息(权)}
基本操作P:
Create_Graph() //图的创建操作。
初始条件:无。
操作结果:生成一个没有顶点的空图G。
GetVex(G,v) //求图中的顶点v的值。
初始条件:图G存在,v是图中的一个顶点。
操作结果:生成一个没有顶点的空图G。
CreateGraph(&G,V,VR)
初始条件:V是图的顶点集,VR是图中弧的集合。
操作结果:按V和VR的定义构造图G。
DFSTraverse(G)
初始条件:图G存在。
操作结果:对图进行深度优先遍历。
BFSTraverse(G)
初始条件:图G存在。
操作结果:对图进行广度优先遍历。
}ADT Graph
三. 图的存储结构
(1)数组表示法(邻接矩阵表示法)
(a)邻接矩阵
图没有顺序存储结构,但可以借助二维数组来表示元素间的关系。
建立一个顶点表vexs(记录各个顶点信息)和一个邻接矩阵arcs(表示各个顶点之间关系)。设图A= (V,E)有n个顶点,则
图的邻接矩阵是一个二维数组A. arcs[n][n],定义为:
![](https://i-blog.csdnimg.cn/blog_migrate/6be1e5c33e23a92e174f1e2c953c7024.png)
分析1:无向图的邻接矩阵是对称的;
分析2:顶点i的度=第i行(列)中1的个数;
特别:完全图的邻接矩阵中,对角元素为0,其余1。
![](https://i-blog.csdnimg.cn/blog_migrate/e8234bd76c0b33c3887f2c0a6780d42f.png)
在有向图的邻接矩阵中,第i行表示以结点为箭尾的弧(即出度边),顶点的出度=行元素之和;第i列表示以结点
为箭头的弧(即入度边),顶点的入度=列元素之和。则顶点的度=第i行元素之和+第i列元素之和,有向图的邻接矩阵不一定是对称矩阵。
对于网,网是带权的图,它的邻接矩阵就不是简单的0和1,定义如下:
(b)存储表示
邻接矩阵的存储表示:用两个数组分别存储顶点表和邻接矩阵。
#define Maxlnt 32767 //表示极大值,即无穷大
#define MVNum 100 //最大顶点数
typedef char VerTexType; //设顶点的数据类型为字符型,可以根据实际情况修改
typedef int ArcType; //假设边的权值类型为整型,可以根据实际情况修改
typedef struct{
VerTexType vexs[MVNum]; //顶点表
ArcType arcs[MVNum][MVNum]; //邻接矩阵
int vexnum, arcnum; //图的当前点数和边数
}AMGraph; //Adjacency Matrix Graph
下面介绍一下邻接矩阵创建无向网的算法,有向图,有向网,无向图是类似的操作:
- 输入总顶点数和总边数。
- 依次输入点的信息存入顶点表中。
- 初始化邻接矩阵,使每个权值初始化为极大值。
- 构造邻接矩阵。
Status CreateUDN(AMGraph &G){ //采用邻接矩阵表示法,创建无向网G
cin>>G.vexnum>>G.arcnum; //输入总顶点数,总边数
for(i = O; 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] = Maxlnt; //边的权值均置为极大值(无穷大)
for(k = O; k<G.arcnum; ++k){ //构造邻接矩阵
cin>>v1>>v2>>w; //输入一条边所依附的顶点及边的权值
i = LocateVex(G, v1);
j = LocateVex(G, v2); //确定v1和v2在G.vexs中的位置
G.arcs[i][j] = w; //边<v1, v2>的权值置为w
G.arcs[j][ti]= w; //置<v1, v2>的对称边<v2,v1>的权值为w
}//for
return OK;
} //CreateUDN
int LocateVex(AMGraph G, VertexType u){
//图G中查找顶点u,存在则返回顶点表中的下标;否则返回-1
int i;
for(i=O;i<G.vexnum;++i)
if(u == G.vexs[i]) return i;
return -1;
}
(c)优缺点分析
优点:
- 直观、简单、好理解;
- 方便检查任意一对顶点间是否存在边;
- 方便找任一顶点的所有“邻接点”(有边直接相连的顶点);
- 方便计算任一顶点的“度”(从该点发出的边数为“出度”,指向该点的边数为“入度”);
缺点:
- 不便于增加和删除顶点;
- 浪费空间——存稀疏图(点很多而边很少)有大量无效元素(对稠密图(特别是完全图)还是很合算的);
- 浪费时间——统计稀疏图中一共有多少条边,时间复杂度是O(n^2)
(2)链式存储结构(邻接表表示法)
(a)邻接表
顶点:按编号顺序将顶点数据存储在一维数组中;
关联同一顶点的边(以顶点为尾的弧):用线性链表存储;
其中表结点的info指边的权值,观察上面的无向图的邻接表,有以下特点:
- 邻接表不唯一(只要是关联的同一个结点,这些边的顺序可以互换)
- 若无向图中有n个顶点、e条边,则其邻接表需n个头结点和2e个表结点。适宜存储稀疏图。
- 无向图中顶点v_i的度为第i个单链表中的结点数。
对于有向图:邻接表存的是结点向外指出的结点。逆邻接表存的是指向这个结点的结点。
对有向图的(逆)邻接表,顶点的出(入)度为第i个单链表中的结点个数;
对有向图的(逆)邻接表,顶点的入(出)度为整个单链表中邻接点域值是i-1的结点个数;
(b)存储表示
//头结点
typedef struct VNode{
VerTexType data; //顶点信息
ArcNode *firstarc; //指向第一条依附该顶点的边的指针
}VNode,AdjList[MVNum]; //AdjList表示邻接表类型
//说明:例如,AdjList v;相当于: VNode v[MVNum];
//边结点
#define MVNum 100 //最大顶点数
typedef struct ArcNode{
int adjvex; //该边所指向的顶点在头结点数组的位置
struct ArcNode *nextarc; //指向下一条边的指针
OtherInfo info; //和边相关的信息
}ArcNode;
//图的结构定义
typedef struct{
AdjList vertices; //vertices是vertex的复数
int vexnum,arcnum; //图的当前顶点数和弧数
}ALGraph;
有了结构定义之后,下面就可以进行一些操作:
ALGragh G; //定义了邻接表表示的图G
G.vexum=5; G.arcnum=5; //图G包含5个顶点,5条边
G.vertices[1].data= 'b'; //图G中第2个顶点是b
p = G.vertices[1].firtarc; //指针p指向顶点b的第一条边结点
p->adjvex = 4; //p指针所指边结点是到下标为4的结点的边
下面介绍一下用邻接表建立无向网的步骤:
- 输入总顶点数和总边数。
- 建立顶点表:依次输入点的信息存入顶点表中,使每个表头结点的指针域初始化为NULL
- 创建邻接表:依次输入每条边依附的两个顶点,根据查找函数确定两个顶点在顶点表的序号i和j,建立边结点,将此边结点分别插入到v_i和v_j对应的两个边链表的头部。
Status CreateUDG(ALGraph &G){ //采用邻接表表示法,创建无向图G
cin>>G.vexnum>>G.arcnum; //输入总顶点数,总边数
for(i = O; i<G.vexnum; ++i){ //输入各点,构造表头结点表
cin>> G.vertices[i].data; //输入顶点值
G.vertices[i].firstarc = NULL; //初始化表头结点的指针域为NULL
} //for
for(k = O; k<G.arcnum;++k){ //输入各边,构造邻接表
cin>>v1>>v2; //输入一条边依附的两个顶点
i = LocateVex(G, v1);
j = LocateVex(G, v2); //在顶点表的数组中执行查找,返回下标
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的边表头部
} //for
return OK;
} //CreateUDG
(c)优缺点
- 方便找任一顶点的所有“邻接点”;
- 节约稀疏图的空间:需要N个头指针+2E个结点(每个结点至少2个域);
- 方便计算无向图任一顶点的“度”,但是对有向图来说只能计算“出度”;需要构造“逆邻接表”(存指向自己的边)来方便计算“入度”;
- 不方便检查任一对顶点是否存在边;
与邻接矩阵的联系:邻接表中每个链表对应于邻接矩阵中的一行,链表中结点个数等于一行中非零元素的个数。
区别:(1)对于任一确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一(链接次序与顶点编号无关)。(2)邻接矩阵的空间复杂度为O(n^2),而邻接表的空间复杂度为O(n+e)。(3)用途:邻接矩阵多用于稠密图;而邻接表多用于稀疏图。
(d)邻接表的优化:十字链表
后面两部分介绍一下对于邻接表的优化表。
十字链表(Orthogonal List)是有向图的另一种链式存储结构。我们也可以把它看成是将有向图的邻接表和逆邻接表结合起来形成的一种链表。有向图中的每一条弧对应十字链表中的一个弧结点,同时有向图中的每个顶点在十字链表中对应有一个结点,叫做顶点结点。它们的结构如下:
顶点结点data域放置图结点信息,不解释;firstin存放第一条入弧,指向该结点的【有向边结点】的指针;firstout存放第一条出弧,指出该结点的【有向边结点】的指针;
弧结点放置有向边结点信息,tailvex域是方向尾(指出)结点在邻接表的下标,headvex域是方向头(指入)结点在邻接表的下标。例如一条有向边从5->2,那么tailvex=5,headvex=2。hlink和tlink都是指针域,hlink存放弧头相同的下一条弧,tlink存放弧尾相同的下一条弧。
有了十字链表之后,顺着头结点的firstin和firstout就可以很方便的看出出度和入度,从而计算结点的度。
(e)邻接表的优化:邻接多重表
对于无向图,邻接表每条边要存两遍。如何降低空间复杂度?