数据结构第六章——图
图的定义和术语
G = (V, E)
V:顶点(数据元素的)有穷非空集合;
E:边的有穷集合。
无向图:每条边都是无方向的。
有向图:每条边都是有方向的。
完全图:任意两个顶点都有一条边两连
无向完全图:n个顶点 n(n-1)/2条边
有向完全图:n个顶点 n(n-1)条边
稀疏图:有很少边或弧地的图
稠密图:有较多的边或弧的图
网:边/弧带权的图
邻接:有边/弧相连的两个顶点之间的关系
关联(依附):边/弧与顶点之间的关系
顶点的度:与该顶点相关联的边的数目,记为TD(v)
在有向图中,顶点的度等于该顶点的入度和出度之和。
顶点v的入度是以b为终点的有向边的条数,记为ID(v)
顶点的出度是以v为始点的有向边的条数,记作OD(v)
路径:接续的边构成的顶点序列
路径长度:路径上边或弧的数目/权值之和。
第一个顶点和最后一个顶点相同的路径。
简单路径:除路径起点和终点可以相同外,其余顶点均不相同的路径。
简单回路(简单环):除路径起点和终点相同外,其余顶点均不相同的路径。
连通图(强连通图):在无(有)向图G=(V,{E})中,若对任何两个顶点v,u都存在从v到u的路径,则称G是连通图(强连通图)。
权与网:图中边或弧所具有的相关数称为权。标明从一个顶点到另一个定点的距离或耗费。
带权的图称为网。
子网:设有两个图G = (V,{E})、G1 = (V1,{E1}),若V1∈V, E1∈E,则称G1是G的子图。
例如:(b)(c)是(a)的子图
连通分量(强连通分量):无向图的极大连通子图称为G的连通分量
极大连通子图的意思是:该子图是G连通子图,将G的任何不在孩子图中的顶点加入,子图不再连通。
有向图G的极大强连通子图称为G的强连通分量。
极大强连通子图意思是:该子图是G的强连通子图,将D的任何不在该子图中的顶点加入,子图不再是强连通。
极小连通子图:该子图是G的连通子图,在该子图中删除任何一条边,子图不再连通
生成树:包含无向图G所有顶点的极小连通子图。
生成森林:对非连通图,由各个连通分量的生成树的集合
邻接矩阵
数组(邻接矩阵)表示法
建立一个顶点表(记录各个顶点信息)和一个邻接矩阵(表示各个顶点之间关系)
无向图的邻接矩阵是对称的
顶点i的度 = 第i行(列)中1的个数
有向图的邻接矩阵中:
第i行含义:以结点vi为尾的弧(即出度边)
第i列含义:以结点vi为头的弧(即入度边)
顶点的出度= 第i行元素之和。
顶点的入度=第i列元素之和
有向图的邻接矩阵可能是不对称的。
邻接矩阵的存储表示:用两个数组分别存储顶点表和邻接矩阵
#define MaxInt 222222(一个很大的数)
#define MVNum 100//最大顶点数
typedef char VerTexType;
typedef struct{
VerTextType vexs[MVNum];//顶点表
ArcType arcs[MVNum][MVNum];//邻接矩阵
int vexnum. arcnum;//图的当前点数和边数
}AMGraph
采用邻接矩阵表示法创建无向图
算法思想:
- 输入总定点数和总边数
- 依次输入点的信息存入顶点表中
- 初始化邻接矩阵,使每个权值初始化为极大值
- 构造邻接矩阵
Status CreateUDN(AMGraph &G){
//采用邻接矩阵表示法,创建无向网G
cin >> G.vexnum >> arcnum;//输入顶点数和总边数
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;//边的权值置为最大值
for(int k = 0; k < G.arcnum;++k)
cin >> v1 >> v2 >> w;//输入一条边所依附的顶点及边的权值
i = LocateVex(G, v1);
j = LocateVex(G, v2);//确定v1和v2在G中的位置
G.arcs[i][j] = w;//边<v1,v2>的权值置为w
G.arcs[j][i] = G.arcs[i][j];//置<v1, v2>的对称边<v2, v1>的权值为w
return OK
}
int LocateVex(AMGraph G, VertexType u){
//图G种查找顶点u,存在则返回顶点表中的下标,否则返回-1
int i;
for(i = 0; i < G.vexnum; i++)
if(u == G.vexs[i]) return i
return -1;
}
图的邻接表存储表示
typedef struct VNode{
VerTextType data;//顶点信息
ArcNode * firsarc;//指向第一条依附该顶点的边的指针
}VNode, AdjList[MVNum];//AdjList表示邻接表类型
//弧(边)的结点结构
#define MVNum 100//最大顶点数
typedef struct ArcNode{ //边结点
int adjvex;//改变所指向的顶点的位置
strcut ArcNode * nextarc;//指向下一条边的指针
OtherInfo info;//和边相关的信息
}ArcNode;
//图结构的定义
typedef struct{
AdjList vertices;
int vexnum, arcnum;//图的当前顶点数和弧数
}ALGraph;
ALGragh G;//定义了邻接表表示的图G
G.vernum = 6, G.arcnum = 6//图G之包含5个顶点。5条边
G.vertices[1].data = 'b'//图G中的第2个顶点是b
p = G.vertices[1].firstarc;//指针p指向顶点b的第一条边结点
p->adjvex = 4;//p指针所指边结点时到下标4的结点的边
采用邻接表表示法创建无向图
算法思想
- 输入总顶点和总边数
- 建立顶点表
一次输入点的信息存入顶点表中,使每个表头结点的指针域初始化为NULL - 创建邻接表
一次输入每条边所依附的两个顶点,确定两个顶点的序号i和j,建立边结点,将此边结点分别插入到vi和vj对应的两个边链表的头部
Status CreateUDG(ALGraph &G){//采用邻接表表示法,创建无向图G
cin >> G.vexnum >> G.arcnum;//输入总顶点数,总边数
for(int i = 0; i < G.vexnum;++i){//输入各点,构造表头结点表
cin >> G.vertices[i].data;//输入顶点值
G.vertices[i].firstarc = NULL;//初始化表头结点的指针域
}
for(int k = 0; 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;
p2 -> nextarc = G.vertices[j].firstarc;
G.vertices[j].first = p2;//将新结点*p2插入顶点vj的边表头部
}
return OK;
}
邻接矩阵与邻接表表示法的区别:
- 对于任意确定的无向图,邻接矩阵你是唯一的(行列号与顶点编号一致),但邻接表不唯一(链接次序与顶点编号无关)
- 邻接矩阵的空间复杂度为O(n^2),而邻接表的空间复杂度为O(n+e)
用途:邻接矩阵多用于稠密图,而邻接表多用于稀疏图
十字链表是有向图的另一种链式存储结构,我们可也可以把它看成是将有向图的链接表和逆邻接表结合起来形成的一种链表
有向图中的每一条胡对应十字链表中的一个弧结点,同时有向图中的每个顶点在十字链表中对应有一个结点,叫做顶点结点
图的遍历
遍历定义:从已给的连通图中某一顶点触发,沿着一些边访图中所有的顶点,且使乜咯顶点仅被访问一次,就叫做图的遍历,它是图的基本运算。
遍历的实质:找每个顶点的邻接点的过程。
图的特点:图中可能存在回路,且图的任一顶点都看与其它顶点相通,在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点。
图常用的遍历:
- 深度优先搜索遍历DFS(一条道走到黑)
- 广度优先遍历BFS
深度优先遍历(DFS)
- 在访问图中某一起始顶点v后,由v触发,访问它的任意邻接顶点w1
- 再从w1出发,访问与w1邻接但未被访问过的顶点w2
- 然后再从w2出发,进行类似的访问
- 如此进行下午,直到到达所有的邻接顶点都被访问过的顶点u为止
- 接着,退回异步,推导前一次刚访问过的顶点,看看是否还有其它没被访问的邻接顶点
- 如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问
- 如果没有,就再退回一步进行搜索,重复上述过程,知道连通图汇总所有顶点都被访问过为止
深度优先搜索遍历算法的实现
邻接矩阵表示的无向图深度优先遍历实现:
void DFS(AMGraph G, int v){//图G为邻接矩阵类型
cout << v;//访问第v个顶点
visited[v] = true;
for(int w = 0; w < G.vexnum; w++)//依次检查邻接矩阵v所在的行
{
if(G.arcs[v][w] != 0 && (!visited[w]))
DFS(G,w);//w是v的邻接点,如果w未访问,则递归调用DFS
}
}
DFS算法效率分析
用邻接矩阵来表示图,遍历图中每一个顶点都要从头扫描该顶点所在行,时间复杂度为O(n^2)
用邻接表来表示图,虽然有2e个表结点,但只需扫描e个结点即可完成遍历,加上访问n个头结点的时间,时间复杂度为O(n+e)
结论:稠密图适用于在邻接矩阵上进行深度遍历。稀疏图时域在邻接表上进行深度遍历
广度优先搜索(BFS)
方法:从图的某一结点出发,首先一次访问该结点的所有邻接点vi1,vi2,……vin,再按这些顶点被访问的先后次序依次访问与他们相邻接的所有违背访问的顶点。重复此过程,纸质所有顶点被访问为止。
按广度优先非递归遍历连通图G
void BFS(Graph G, int v){
cout << v; visited[v] = true;//访问第v分顶点
InitQueue(Q);//辅助队列Q初始化,置空
EnQueue(Q,v);//v进队
while(!QueueEmpty(Q)){//队列非空
DeQueue(Q, u);//队头元素出队并置为u
for(w = FirstAdjVex(G,u); w >= 0; w = NextAdjVex(G, u, w))
if(!visited[w])//w为u的尚未访问的邻接顶点
cout << w;
visited[w] = true;
EnQueue(Q,w);//进队
}
}
}
BFS算法效率分析
- 如果使用邻接矩阵,则BFS对于每一个被访问到的顶点都要循环检测矩阵中的整整一行(n个元素),总代价为O(n^2)
- 用邻接表来标示图,虽然有2e个表结点,但只需扫描e个结点即可完成遍历,加上访问n个头结点的时间,时间复杂度为O(n + e)。
生成树:所有的顶点均有边连接在一起,但不存在回路的图
一个图可以有许多棵不同的生成树
所有的生成树具有以下共同特点
- 生成树的顶点个数与图的顶点个数相同
- 生成树是图的极小连通子图,去掉一条边则非连通
- 一个有n个顶点的连通图的生成树有n-1条边
- 在生成树中再加一条边必然形成回路
- 生成树中任意两个顶点间的路径是唯一的
一个图中可以有许多不同的生成树。
含有n个顶点n-1条边的图不一定是生成树。
最小生成树:给定一个无向网络,在该网的所有生成树中,使得各边权值之和最小的那棵生成树称为该网的最小生成树。也称最小代价生成树。
构造最小生成树
构造最小生成树的算法很多,其中多数算法都利用了MST性质
MST性质:设N = (V,E)是一个连通网,U是顶点集V的一个非空子集。若边(u,v)s是一条具有最小权值的边,其中u∈U,v属于V-U,则必存在一棵包含(u,v)的最小生成树。
MST性质解释
在生成树的构造过程中,图中n个顶点分属两个集合:
已落在生成树上的顶点集:U
尚未若在生成树上的顶点集V-U
接下来则应在所有连通U中顶点和V-U中顶点的边中选取权值最小的边。
算法思想:
- 设N = (V,E)是连通网,TE是N上最小生成树中边的集合
- 初始令U = {u0},{u0∈V},TE={}
- 在所有u∈U,v∈V-U的边(u,v)∈E中,找一条代价最小的边(u0,v0)
- 将(u0,v0)并入集合TE,同时v0并入U0
- 重复上述操作直至U=V为止,则T = (V,TE)为N的最小生成树。
构造最小生成树方法二:克鲁斯卡尔(kruskal)算法
算法思想:
- 设连通网N = (V,E),令最小生成树初始状态为只有n个顶点儿无边的非连通图T=(V,{}),每个顶点自成一个连通分量
- 在E中选取代价最小的边,若该边衣服的顶点落在T中不同的连通分量上(即:不能形成环),则将此边加入到T中,否则,舍去此边,选取下一条代价最小的边
- 依次类推,直到T中所有顶点都在同一连通分量上为止
最短路径:
问题抽象:在有向网中A点(源点)到达B点(终点)的多条路径,寻找一条各边权值之和最小的路径,即最短路径。
最短路径与最小生成树不同,路径上不一定包含n个顶点,也不一定包含n-1条边。
两种常见的最短路径问题:
- 单源最短路径——用Dijkstra(迪杰斯特拉)算法
- 所有顶点间的最短路径——用Floyd(弗洛伊德)算法
Dijstra算法
- 初始化:先找出从源点v0到各终点vk顶点直达路径(v0,vk),即通过一条弧到达的路径
- 选择:从这些路径中找出一条长度最短的路径
- 更新:然后对其余各路进行适当调整。
Floyed算法
- 逐个顶点试探
- 从vi到vj的所有可能存在的路径中
- 选出一条长度最短的路径
步骤:
- 初始时设置一个n阶方阵,对齐对角线元素设置为0,若存在弧<vi,vj>,则对应元素为权值,否则为∞
- 逐步试着在原直接路径中增加中间顶点,若加入中间顶点庵后路径变短,则修改之,否则,位置原值。所有顶点试探完毕,算法结束。
有向无环图:无环的 有向图,简称DAG图
AOV网:用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网简称AOV网
AOE网:用一个有向图表示一个工程的各子工程及其相互制约的关系,以弧表示活动,以顶点表示活动的开始或结束事件,称这种有向图为边表示活动的网,称为AOE网
AOV网的特点:
- 若从i到j有一条有向路径,则i是j的前驱,j是i的后继
- 若<i,j>是网中有向边,则i是j的直接前驱,j是i的直接后继
- AOV网中不允许有活动,因为如果有回路存在,则标明某项活动以自己为先决条件,显然这是荒谬的
拓扑排序
在AOV网没有回回路的前提下,我们将全部活动排列成一个线性序列,使得若AOV网中有弧<i,j>存在,则在这个序列中,i一定排在j的前面,具有这种性质的线性序列称为拓扑有序序列,相应的拓扑有序排序的算法称为拓扑排序。
拓扑排序的方法:
- 在有向体育中选一个没有前驱的顶点且输出之。
- 从有向图中删除该顶点和所有以它为尾的弧
- 重复上述步骤,直至全部顶点均已输出或者当图中不存在无前驱的顶点为止。
一个AOV网的拓扑序列不是唯一的。
检测AOV网中是否存在环方法:对该有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必定不存在环
关键路径
把工程计划表示为边表示活动的网络,即AOE网,同顶底表示事件,弧表示活动,弧的权表示活动持续时间
事件表示在他之前的活动已经完成,在它之后的活动可以开始。
关键路径——路径长度最长的路径
路径长度——路径上各活动持续时间之和
列,使得若AOV网中有弧<i,j>存在,则在这个序列中,i一定排在j的前面,具有这种性质的线性序列称为拓扑有序序列,相应的拓扑有序排序的算法称为拓扑排序。
拓扑排序的方法:
- 在有向体育中选一个没有前驱的顶点且输出之。
- 从有向图中删除该顶点和所有以它为尾的弧
- 重复上述步骤,直至全部顶点均已输出或者当图中不存在无前驱的顶点为止。
一个AOV网的拓扑序列不是唯一的。
检测AOV网中是否存在环方法:对该有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必定不存在环
关键路径
把工程计划表示为边表示活动的网络,即AOE网,同顶底表示事件,弧表示活动,弧的权表示活动持续时间
事件表示在他之前的活动已经完成,在它之后的活动可以开始。
关键路径——路径长度最长的路径
路径长度——路径上各活动持续时间之和