无题
6 图
图是一种比线性表和树更为复杂的数据结构。
6.1 图的定义和基本术语
6.1.1 图的定义
-
图:G=(V.E)
- V:顶点(数据元素)的有穷非空集合;
- E:边的有穷集合
-
无向图:每条边都是无方向的
-
有向图:每条边都是有方向的
-
完全图:任意两个点都有一条边相连
-
稀疏图:有很少边或弧的图($e<nlog_2n$)
-
稠密图:有较多边或弧的图
6.1.2 图的基本术语
-
网,边/弧带权的图
-
邻接:有边/弧相连的两个顶点之间的关系。
- 存在($v_i,v_j$),则称为$v_i和v_j$互为邻接点
- 存在<$v_i,v_j$>,则称$v_i$邻接到$v_j$,$v_j$邻接于$v_i$,
-
关联(依附):边/弧与顶点之间的关系。
- 存在($v_i,v_j$)/<$v_i,v_j$>,则称该边/弧关联于$v_i$和$v_j$
-
顶点的度:与该顶点相关联的边的数目,记为TD(v)
-
在有向图中,顶点的度等于该顶点的入度和出度之和。
-
顶点v的入度是以v为终点的有向边的条数,记作ID(v)
-
顶点v的出度是以v为始点的有向边的条数,记作OD(v)
-
-
路径:连续的边构成的顶点序列
-
路径长度:路径上边或弧的数目/权值之和
-
回路(环):第一个顶点和最后一个顶点相同的路径
-
简单路径:除路径起点和终点可以相同外,其余顶点均不相同的路径
-
简单回路(环):除路径起点和终点相同外,其余顶点均不相同的路径
-
连通图(强连通图):在无(有)向图G=(V,{E})中,若对任意两个顶点v、u都存在从v到u的路径,则称G是连通图(强连通图)
-
权与网:
- 图中边或弧所具有的相关数称为权,表明从一个顶点到另一个顶点的距离或耗费。
- 带权的图称为网
-
子图:设有两个图$G=(V,{E})、G_1=(V_1,{E_1})$,若$V_1属于V,E_1属于E$则称$G_1是G$的子图
-
连通分量(强连通分量)
-
无向图G的极大联通子图称为G的连通分量
- 极大连通子图意思是:该子图是G联通子图,将G的任何不在该子图汇总的顶点加入,子图不再连通
-
有向图G的极大连通子图称为G的强连通分量
- 极大强连通子图意思是: 该子图是G的强连通子图,将D的任何不在该子图中的顶点加入,子图不再是强连通的。
-
极小联通子图:该子图是G的连通子图,在该子图中删除任何一条边,联通子图不再连通
-
生成树:包含无向图G所有顶点的极小连通子图
-
生成森林:对非连通图,由各个连通分量的生成树的集合
-
6.2 案例引入
6.2.1 六度空间理论
理论又称作六度分隔论 (Six Degrees of Separation)。六度空间理论是20世纪60年代由美国的心理学家米格兰姆(Stanley Milgram)提出的,理论指出:“你和任何一个陌生人之间所间隔的人不会超过六个”
6.3 图的类型定义
图的抽象数据类型定义如下:
1 | ADT Graph{ |
-
基本操作:
-
CreateGraph{&G,V,VR}
初始条件:V是图的顶点集,VR是图中弧的集合。
操作结果:按V和VR的定义构造图G。
-
DFSTraverse(G)
初始条件:图G存在。
操作结果:对图进行深度优先遍历,在遍历过程中对每个顶点访问一次。
-
BFSTraverse(G)
初始条件:图G存在。
操作结果:对图进行广度优先遍历,在遍历过程中对每个顶点访问一次.
-
6.4 图的存储结构
- 图的逻辑结构:多对多
- 图没有顺序存储结构,可以借助二维数组来表示元素间的关系
- 数组表示法(邻接矩阵)
- 链式存储结构:
- 多重链表
- 邻接表
- 邻接多重表
- 十字链表
- 多重链表
- 重点介绍:
- 邻接矩阵(数组)表示法
- 邻接表(链式)表示法
6.4.1 邻接矩阵
-
数组(邻接矩阵)表示法
-
建立一个顶点表(记录各个顶点信息)和一个邻接矩阵(表示各个顶点之间关系)。
-
设图A=(V,E)有n个顶点,则
-
顶点表Vexs[n]
i 1 2 3 …… n-1 Vexs[n] $V_1$ $V_2$ $V_3$ …… $V_n$ -
图的邻接矩阵是一个二维数组$A.arcs[n][n]$,定义为:
-
$A.arcs[i][j]=\quad\begin{cases}1,如果<v_i,v_j>或(v_i,v_j)\in E\0,反之\end{cases}$
-
-
无向图的邻接矩阵表示法:
- 无向图的邻接矩阵是对称的
- 顶点i的度=第i行(列)中1的个数;
- 完全图的邻接矩阵中,对角元素为0,其余1.
-
有向图的邻接矩阵表示法:
- 有向图的邻接矩阵可能是不对称的
- 顶点的出度=第i行元素之和
- 顶点的入度=第i列元素之和
- 顶点的度=第i行和列的元素之和
-
网(即有权图)的邻接矩阵表示法
-
邻接矩阵的存储表示:用两个数组分别存储顶点表和邻接矩阵
1
2
3
4
5
6
7
8
9
typedef char VerTexType; //设顶点的数据类型为字符型
typedef int ArcType; //假设边的权值类型为整形
typedef struct{
VerTexType vexs [MVNum]; //顶点表
ArcType arcs[MVNum) [MVNum]; //邻接矩阵
int vexnum,arcnum; //图的当前点数和边数
}AMGraph;
-
-
采用邻接矩阵表示法创建无向网
- 输入总顶点数和总边数。
- 依次输入点的信息存入顶点表中。
- 初始化邻接矩阵, 使每个权值初始化为极大值。
- 构造邻接矩阵。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15Status CreateUDN(AMGraph &G){//创建无向网G
cin>>G.vexnum>>G.arcnum;//输入总顶点数,总边数
for(i=O;i<G.vexnum;++i)
cin>>G.vexs[i];//依次输入点的信息
for(i=O;i<G.vexnum;++i)//初始化邻接矩阵呢
for (j=0; j<G.vexnum;++j)
G.arcs[i][j]=Maxint;//变得权值均置为最大
for(k=O;k<G.arcnum;++k){//构造邻接矩阵
cin>>vl>>v2>>w;//输入一条边所依附的顶点及边的权值
i=LocateVex(G,vl);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;
}补充算法:在图中查找顶点:
1
2
3
4
5
6int LocateVex(AMGraph G,VertexType u){
int i;
for(i=0;i<G.vexnum;++i)
if(u==G.vexs[i]) return i;
return -1;
}
-
-
邻接矩阵的优点
- 直观简单好理解
- 方便检查任意一对顶点间是否存在边
- 方便找任一顶点的所有“邻接点”
- 方便计算任一顶点的“度”
-
邻接矩阵的缺点
- 不便于增加和删除顶点
- 浪费空间——存稀疏图有大量无效元素
- 对稠密图来说还是很合算的
- 浪费时间——统计稠密图中一共有多少条边
6.4.2 邻接表
-
无向图邻接表表示法(链式)
- 顶点:按编号顺序将顶点数据存储在一维数组中;
- 关联同一顶点的边(以顶点为尾的弧):
- 用线性链表存储
- 特点:
- 邻接表不唯一
- 若无向图中有n个顶点、e条边,则其邻接表需n个头结点和2e个表结点。适宜存储稀疏图
- 无向图中顶点$V_i$的度为第i个单链表中的结点数
-
有向图邻接表表示法
- 特点:
- 顶点$V_i$的出度为第i个单链表中的结点个数
- 顶点的入度为整个链表中领接点域值是i-1的结点个数
- 特点:
-
图的邻接表存储表示:
1
2
3
4typedef struct VNode{
VerTexType data; //顶点信息
ArcNode *firstarc; //指向第一条依附该顶点的边的指针
}VNode,AdjList[MVNum] //AdjList表示邻接表类型弧(边)的结点结构 adjvex | nextarc | info
1
2
3
4
5
6
typedef struct ArcNode{ //边结点
int adjvex; //该边所指向的顶点的位置
struct ArcNode * nextarc; //指向下一条边的指针
OtherInfo info; //和边相关的信息
}ARcNode;图的结构定义:
1
2
3
4typedef struct{
AdjList vertices;//vertices——vertex的复数
int vexnum,arcnum;//图的当前顶点数和弧数
}ALGraph;邻接表操作举例说明:
-
无向图的邻接表表示 p118
1 | Status CreateUDG(ALGraph &G) { //采用邻接表表示法, 创建无向图 G |
-
无向图邻接表特点:
- 方便找任一顶点的所有“邻接点”
- 节约稀疏图的空间
- 需要n个头指针和+2e个结点(每个结点至少2个域)
- 方便计算任一顶点的“度”
- 对无向图:是的
- 对有向图:只能计算“出度”需要构造“逆邻接表”来方便计算“入度”
- 不方便检查任意一对顶点间是否存在边
-
邻接矩阵与邻接表表示法的关系
- 联系:邻接表中每个链表对应于接短阵中的一行,链表中结点个数等于一行中非零元素的个数。
- 区别:
- 对于任一确定的无向图,邻接矩阵是唯一的 (行列号与顶点编号一致),但邻接表不唯一 (链接次序与顶点编号无关)
- 邻接矩阵的空间复杂度为$O(n^2)$,而邻接表的空间复杂度为$O(n+e)$。(e是出于$0到n^2$之间的复杂变量)
- 用途:邻接矩阵多用于稠密图;而邻接表多用于稀疏图
6.4.3 十字链表
有向图:
- 顶点结点:
data | firstin | firstout |
---|---|---|
存与顶点相关的信息 | 入度边 | 出度边 |
- 弧结点:
tailvex | headvex | hlink | tlink |
---|---|---|---|
弧尾位置 | 弧头位置 | 弧头相同的下一条弧 | 弧尾相同的下一条弧 |
弧头是箭头,弧尾是箭尾
6.4.4 邻接多重表
边结点:
mark | ivex | ilink | jvex | jlink | info |
---|---|---|---|---|---|
标志域,标记此边是否被搜索过 | 该边依附的两个顶点在表头数组中的位置 | 指向依附于ivex的下一条边 | 该边依附的两个顶点在表头数组中的位置 | 指向依附于jvex的下一条边 |
顶点节点
data | firstedge |
---|---|
存与顶点有关的信息 | 指向第一条依附于该顶点的边 |
6.5 图的遍历
-
遍历的实质:找每个顶点的邻接点的过程
-
图常用的遍历:
- 深度优先搜索(DFS)
- 广度优先搜索(BFS)
6.5.1 深度优先搜索
甚多有限搜索遍历过程:
- 从图中某个顶点v出发, 访问v。
- 找出刚访问过的顶点的第一个未被访问的邻接点, 访问该顶点。 以该顶点为新顶点,重复此步骤, 直至刚访问过的顶点没有未被访问的邻接点为止。
- 返回前一个访问过的且仍有未被访问的邻接点的顶点,找出该顶点的下一个未被访问的邻接点, 访问该顶点。
- 重复步骤 (2)和(3), 直至图中所有顶点都被访问过,搜索结束。
6.5.1.1 采用邻接矩阵表示图的深度优先搜索遍历
1 | void DFS(AMGraph G,int v){//图G为邻接矩阵类型 |
- DFS算法效率分析
- 用邻接矩阵来表示图,遍历图中每一个顶点都要从头扫描该顶点所在行,时间复杂度为$O(n^2)$
- 用邻接表来表示图,虽然有2e个表结点,但只需扫描 e 个结点即可完成遍历,加上访问 n个头结点的时间,时间复杂度为$O(n+e)$
- 结论
- 稠密图适合在邻接矩阵上进行深度遍历
- 稀疏图适合在邻接表上进行深度遍历
6.5.2 广度优先搜索
- 用队列实现广度优先遍历,累次层次遍历那样
6.5.2.1 按广度优先非递归遍历连通图G
1 | void BFS (Graph G,int v){ |
- BFS算法效率分析
- 如果邻接矩阵,则BFS对于每一个背访问到的顶点,都要循环检测矩阵中的整整一行(n个元素),总的时间代价为$O(n^2)$
- 用邻接表来表示图,虽然有 2e 个表结点,但只需扫描 e 个结点即可完成遍历,加上访问 n个头结点的时间,时间复杂度为$O(n+e)$
- DFS和BFS算法效率比较
- 空间复杂度相同,都是O(n)(借用了堆栈或队列);
- 时间复杂度只与存储结构(邻接矩阵或邻接表)有关,而与搜索路径无关。
6.6 图的应用
6.6.1 最小生成树
-
生成树:所有顶点均由边连接在一起,但不存在回路的图
-
一个图可以有许多棵不同的生成树
-
所有生成树具有以下共同特点
- 生成树的顶点个数与图的顶点个数相同
- 生成树是图的极小连通子图,去掉一条边则非连通
- 一个有n个顶点的连通图的生成树有n-1条边
- 在生成树中再加一条边必然形成回路
- 生成树中任意两个顶点间的路径是唯一的
-
-
无向图的生成树:
- 设图G=(V,E)是个连通图,当从图任一顶点出发遍历图G时,将边集E(G)分成两个集合T(G) 和B(G)。其中 T(G)是遍历图时所经过的边的集合,B(G)是遍历图时未经过的边的集合。显然,G1(V,T)是图G的极小连通子图。即子图G1 是连通图G的生成树
-
最小生成树
-
给定一个无向网络,在该网络的所有生成树中,使得各边权值之和最小的那棵生成树称为该网的最小生成树,也叫最小代价生成树。
-
最小生成树的典型用途
- 要在n个城市间建立通信网,则n个城市应铺设n-1条路,每条路也有对应的经济成本
- 建立数学模型:
- 顶点 代表城市 有n个
- 边 代表线路 有n-1条
- 边的权值 表示线路的经济代价
- 连通网 表示n个城市间的通信网
-
-
构造最小生成树
- 构造最小生成树的算法很多,其中多数算法都利用了MST的性质
- MST性质:设N=(V,E) 是一个连通网,U是顶点集V的一个非空子集。若边(u,v) 是一条具有最小权值的边,其中$u\in U,v \in V-U$则必存在一棵包含边(u,v)的最小生成树
- 在生成树的构造过程中,图中n个顶点分属两个集合:
- 已落在生成树上的顶点集: U
- 尚未落在生成树上的顶点集: V-U
- 接下来则应在所有连通U中顶点和V-U中顶点的边中选取权值最小的边
6.6.1.1 普利姆(prim)算法
-
算法思想:
-
设N=(V,E)是连通网,TE是N上最小生成树 中边的集合
-
初始令$U={u_0},(u_0 \in V),TE=${}
-
在所有$u\in U,v \in V-U$的边$(u,v)\in E$中找一条代价最小的边$(u_0,v_0)$.
-
将$(u_0,v_0)$并入集合TE,同时$v_o$并入U
-
重复上述操作直至U=V为止,则T=(V,TE)为N的最小生成树
-
6.6.1.2 克鲁斯卡尔(Kruskal)算法
- 算法思想:
- 设连通网 N=(VE),令最小生成树初始状态为只有 n个顶点而无边的非连通图T=(V,{}),每个顶点自成一个连通分量。
- 在E中选取代价最小的边,若该边依附的顶点落在T中不同的连通分量上(即:不能形成环),则将此边加入到 T中;否则,舍去此边,选取下一条代价最小的边。
- 依此类推,直至T中所有顶点都在同一连通分量上为止。
- 最小生成树可能不唯一
算法名 | 普里姆算法 | 克鲁斯卡尔算法 |
---|---|---|
算法思想 | 选择点 | 选择边 |
时间复杂度 | O(n^2)(n为顶点数) | O(eloge)(e为边数) |
适应范围 | 稠密图 | 稀疏图 |
6.6.2 最短路径
- 两种最常见的最短路径问题:
- 单源最短路径——用Dijkstra(迪杰斯特拉)算法
- 所有顶点间的最短路径——用Floyd——(弗洛伊德)算法
6.6.2.1 迪杰斯特拉算法
- 初始化:从源点$v_0$到各终点$v_k$的直达路径 $(v_o,v_k)$,即通过一条弧到达的路径。
- 选择:从这些路径中找出一条长度最短的路径$(v_0,u)$.
- 更新:然后对其余各条路径进行适当调整:
- 若在图中存在弧$(u,v_k)$,且$(v_0,u)+(u,v_k)<(v_0,v_k),则以路径(v_0,u,v_k)代替(v_0,v_k)$
- 在调整后的各条路径中,再找长度最短的路径,依此类推
时间复杂度为 $O(n^3)$
6.6.2.2 弗洛伊德算法
6.6.3 拓扑排序
-
有向无环图:无环的有向图,简称DAG图
- 有向无环图常用来描述一个工程或系统的进行过程。
- 一个工程可以分为若干个子工程,只要完成了这些子工程,就可以导致整个工程的完成
-
AOV网 拓扑排序
- 用一个有向图表示一个工程的各子工程及其相只制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网,简称 AOV网(Activity On Vertex network)。
-
AOE网 关键路径
- 有向图表示一个工程的各子工程及其相互制约的关系,以弧表示活动,以顶点表示活动的开始或结束事件,称这种有向图为边表示活动的网,简称为AOE网 (Activity On Edge)
-
AOV网的特点:
- 若从i到j有一条有向路径,则i是j的前驱j是i的后继
- 若<i,j>是网中有向边,则i是j的直接前驱;j是i的直接后继
- AOV网中不允许有回路,因为如果有回路存在,则表明某项活动以自己为先决条件,显然这是荒谬的。
-
拓扑排序
- 在AOV 网没有回路的前提下,我们将全部活动排列成一个线性序列,使得若 AOV 网中有弧 <i,j>存在,则在这个序列中,i一定排在的前面,具有这种性质的线性序列称为拓扑有序序列,相应的拓扑有序排序的算法称为拓扑排序
-
拓扑排序方法:
- 在有向图中选一个没有前驱的顶点且输出
- 在图中删除该顶点和所有以他为尾的弧
- 重复1和2直到全部顶点均已输出;当图中不存在无前驱的顶点位置
- 一个AOV网的拓扑序列不是唯一的
-
拓扑排序的一个重要应用:
- 检测AOV网中是否存在环的方法:
- 对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该 AOV 网必定不存在环。
- 检测AOV网中是否存在环的方法:
-
关键路径
-
假设一个工程有11项活动,9个事件
- 事件V1——表示整个工程开始(源点:入度为0的顶点)
- 事件v9——表示整个工程结束(汇点:出度为0的顶点)
- 关键路径——路径长度最长的路径
- 路径长度——路径上各活动持续时间之和
- ve(vj)——表示事件vj的最早发生时间
- vl(vj)——表示事件vj的最迟发生时间
- e(i)——表示活动ai的最早开始时间
- l(i)——表示活动ai的最迟开始时间
- l(i)-e(i)——表示完成活动ai的时间余量
- 关键活动——关键路径上的活动,即l(i)==e(i)的活动(即没有时间余量的活动)
- 其中√的为关键活动,而包含关键活动的路径为关键路径
- 需要加快同时在几条关键路径上的关键活动
- 如:a11,a10,a8,a7
- 如果一个活动处于所有关键路径上,那么提高这个活动的速度,就能缩短整个工程的完成时间
- 如:a1,a4
- 处于所有关键路径上的活动完成时间不能缩短太多,否则会使原来的关键路径变成不是关键路径。这时必须重新寻找关键路径
- 需要加快同时在几条关键路径上的关键活动