数据结构-图
文章目录
- 6.1 图的基本概念
- 6.1.1 图的定义
- 6.2 图的存储及基本操作
- 6.2.1 邻接矩阵法
- 6.2.2 邻接表法
- 6.2.3 十字链表
- 6.2.3 邻接多重表
- 6.2.5 图的基本操作
- 6.3 图的遍历
- 6.3.1 广度优先遍历
- 6.3.1 深度优先遍历
- 6.4 图的应用
- 6.4.1 最小生成树
- 6.4.2 最短路径
- 6.4.3 有向无环图描述表达式
- 6.4.4 拓扑排序
- 6.4.5 关键路径
6.1 图的基本概念
6.1.1 图的定义
🎈图由顶点集V和边集E组成,记为G(V,E),其中V表示图G中顶点的有限非空集,E表示图G中顶点之间的关系集合。
✅无向图: 若E是无向边的有限集合时,则图G为无向图
✅有向图: 若E是有向边(也称为弧)的有限集合时,则图G为有向图
✅简单图: 不存在重复的边 ;不存在顶点到自身的边
✅多重图: 图G中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联
✅完全图:
- 有向完全图:任意两个顶点之间存在方向相反的两条弧,n*(n-1) 条弧边
- 无向完全图:任意两个顶点之间都存在边,n*(n-1)/2条边
✅子图: 设有两个图G (V,E)和G’=(V’,E’),若V’是V的子集,且E’是E的子集则称G’是G的子图
✅连通图:
-
连通:若从顶点V到顶点W有路径存在,则称V和W是连通的
-
连通图:任意两个顶点都是连通的
-
非连通图:如果一个图中有n个顶点,并且有小于n-1条边,则此图必是非连通图
-
极大连通图(连通分量):
- 是一个子图
- 是连通的
- 顶点足够多
- 极大连通子图包含依附这些顶点的所有边
-
强连通:从顶点V到W,和从顶点W到V之间都有路径
假设一个有向图有n个顶点,如果是强连通图,那么最少需要 n 条边(形成回路)
✅生成树:
- 连通图的生成树是包含图中的全部顶点的一个极小连通子图
- 若图中顶点数为n,则它的生成树含有n-1条边
- 生成树去掉一条边则变成非连通图,加上一条边就会有回路
✅度:
- 无向图:度之和=2倍的边数
- 有向图:入度,出度,入度和=出度和=边数,度之和=出度+入度
✅权和网: 在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。这种边上带有权值的图称为带权图,也成网
✅稠密图、稀疏图: 边数很少的称为稀疏图,反之称为稠密图
✅路径、路径长度、回路(环): 路径:顶点到另外一个顶点的序列;路径长度:路径上边的数目;回路(或环):第一个顶点和最后一个顶点相同的路径
✅简单路径、简单回路: 简单路径:顶点不重复;简单回路:除第一个顶点和最好一个顶点外,其余顶点不重复出现的回路
✅距离: 从顶点u到顶点v的最短路径存在,则此路径长度为距离;没有则为oo
✅有向树: 一个顶点的入度为0,其余顶点的入度均为1的有向图
6.2 图的存储及基本操作
6.2.1 邻接矩阵法
🎈—维数组存储顶点信息,二维数组存储边或弧信息(即各顶点之间的邻接关系)
A[ i ][ j ]:1,顶点到另一个的边或弧 ;0,顶点与这个顶点没有边或弧
#define MaxVertexNum 100 //顶点数目的最大值
typedef char VertexType; //顶点的数据类型
typedef int EdgeType; //带权图中边上权值的数据类型
typedef struct{
VertexType Vex[MaxVertexNum]; //顶点表
EdgeType Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexnum,arcnum; //图的当前顶点数和弧数
}MGraph;
✅无向图:
- 第i个结点的度=第i行(或第i列)的非零元素个数
- 无向图的邻接矩阵是对称矩阵,可以压缩存储
✅有向图:
- 第i个结点的出度=第i行的非零元素个数
- 第i个结点的入度=第i列的非零元素个数
- 第i个结点的度=第i行、第i列的非零元素个数之和
邻接矩阵法求顶点的度/出度/入度的时间复杂度为o(IVI)
🔴优点:
- 便于判断两个顶点之间是否有边, 即根据A[ü][]=0或1来判断
- 便于计算各个顶点的度。 对于无向图,邻接矩阵第i行元素之和就是顶点i的度;对于有向图,第i行元素之和就是顶点i的出度,第i列元素之和就是顶点i的入度
🔵缺点:
- 不便于增加和删除顶点
- 不便于统计边的数目,需要扫描邻接矩阵所有元素才能统计完毕,时间复杂度为O(n2)
- 空间复杂度高。 如果是有向图,n个顶点需要n2个单元存储边。如果是无向图,因其邻接矩阵是对称的,所以对规模较大的邻接矩阵可以采用压缩存储的方法,仅存储下三角(或上三角)的元素,这样需要n(n-1)/2个单元即可
✅性质:
- 稠密图适合使用邻接矩阵的存储表示
- 设图G的邻接矩阵为A,An的元素An [ i ][ j ]等于由顶点 i 到顶点 j 的长度为 n 的路径的数目。
6.2.2 邻接表法
#define MaxVertexNum 100 //图中顶点数目的最大值
typedef struct ArcNode{ //边表结点
int adjvex; //该弧所指向的顶点的位置
struct ArcNode *next; //指向下一条弧的指针
InfoType info; //网的边权值
}ArcNode;
typedef struct VNode{ //顶点表结点
VertexType data; //顶点信息
ArcNode *first; //指向第一条依附该顶点的弧的指针
}VNode,AdjList[MaxVertexNum];
typedef struct{
AdjList vertices; //邻接表
int vexnum,arcnum; //图的顶点数和弧数
}ALGraph; //ALGraph是以邻接表存储的图类型
🔴优点:
- 便于增加和删除顶点
- 便于统计边的数目,按顶点表顺序扫描所有边表可得到边的数目,时间复杂度为O(n+e)
- 空间效率高
🔵缺点:
- 不便于判断顶点之间是否有边,要判定Vi和Vj之间是否有边,就需扫描第i个边表,最坏情况下要耗费Оn)时间
- 不便于计算有向图各个顶点的度
6.2.3 十字链表
🎈十字链表是有向图的一种链式存储结构,空间复杂度:O(|V|+|El)
在十字链表中容易求得顶点的出度和入度
如何找到指定顶点的所有出边? 顺着绿色线路找
如何找到指定顶点的所有入边? 顺着橙色线路找
图的十字链表的表示不唯一,但一个十字链表表示确定一个图
6.2.3 邻接多重表
🎈十字链表是无向图的一种链式存储结构,空间复杂度:O(/V|+|E|)
6.2.5 图的基本操作
6.3 图的遍历
6.3.1 广度优先遍历
🎈广度优先遍历类似于树的按层次遍历的过程
✅算法步骤
- 从图中某个顶点v出发,访问v,并置visited[v]的值为true,然后将v进队
- 只要队列不空,则循环下述操作:队头顶点v出队,依次检查v的所有邻接点w,如果visited[w]的值为false,则访问w,并置visited[w]的值为true,然后将w进队
同一个图的邻接矩阵表示方式唯一,因此广度优先遍历序列唯一
同一个图邻接表表示方式不唯一,因此广度优先遍历序列不唯一
✅代码实现
bool visited [MAX_VERTEX_NUM]; //访问标记数组
void BFSTraverse(Graph G){ //对图G进行广度优先遍历
for( i=0; i<G.vexnum;++i)
visited[i]=FALSE; //访问标记数组初始化
InitQueue(Q) ; //初始化辅助队列Q
for( i=0; i<G.vexnum;++i) //从0号顶点开始遍历
if( !visited [i]) //对每个连通分量调用一次BFS
BFS(G,i); // vi未访问过,从vi开始BFS
}
//广度优先遍历
void BFS(Graph G,int v){ //从顶点v出发,广度优先遍历图G
visit(v); //访问初始顶点v
visited[v]=TRUE; //对v做已访问标记
Enqueue(Q,v); //顶点v入队列Q
while( ! isEmpty(Q) ){
DeQueue(Q,v); //顶点v出队列
for(w=FirstNeighbor(G, v);w>= ; w=NextNeighbor(G,v,w))
//检测v所有邻接点
if( !visited [w]){ //w为v的尚未访问的邻接顶点
visit(w); //访问顶点w
visited[w]=TRUE; //对w做已访问标记
EnQueue(Q,w); //顶点w入队列
}
}
}
✅复杂度分析
时间复杂度:
邻接矩阵:O(|V|2)
邻接表:O(|V|+|E|)
空间复杂度:
邻接矩阵:O(|V|)
邻接表:O(|V|)
✅广度优先生成树
广度优先生成树由广度优先遍历过程确定。由于邻接表的表示方式不唯一,因此基于邻接表的广度优先生成树也不唯一。
对非连通图的广度优先遍历,可得到广度优先生成森林
6.3.1 深度优先遍历
🎈深度优先遍历类似于树的先序遍历,是树的先序遍历的推广
✅算法步骤
- 从图中某个顶点v出发,访问v,并置visited[v]的值为true
- 依次检查v的所有邻接点w,如果visited[w]的值为false,再从w出发进行递归遍历,直到图中所有顶点都被访问过
- ==注意:==若是非连通图,上述遍历过程执行之后,图中一定还有顶点未被访间,需要从图中另选一个未被访问的顶点作为起始点,重复上述深度优先搜索过程,直到图中所有顶点均被访问过为止
✅代码实现
bool visited [MAX_VERTEX_NUM]; //访问标记数组
void DFSTraverse(Graph G){ //对图G进行深度优先遍历
for( v=0; v<G.vexnum;++v)
visited[v]=FALSE; //初始化已访问标记数据
for( v=; v<G.vexnum;++v) //本代码中是从v=0开始遍历
if( !visited[v])
DFS(G,v);
}
void DFS(Graph G,int v){ //从顶点v出发,深度优先遍历图G
visit(v); //访问顶点v
visited [v]=TRUE; //设已访问标记
for(w=FirstNeighbor(G,v); w>=0;w=NextNeighor(G,v,w))
if( !visited [w] ){ //w为u的尚未访问的邻接顶点
DFS(G,w);
}
}
✅复杂度分析
时间复杂度:
邻接矩阵:O(|V|2)
邻接表:O(|V|+|E|)
空间复杂度:
邻接矩阵:O(|V|)
邻接表:O(|V|)
同一个图的邻接矩阵表示方式唯一,因此深度优先遍历序列唯一 ,深度优先生成树也唯一
同一个图邻接表表示方式不唯一,因此深度优先遍历序列不唯一,深度优先生成树也不唯一
6.4 图的应用
6.4.1 最小生成树
🎈连通图的生成树是包含图中全部顶点的一个极小连通子图。
若图中顶点数为n,则它的生成树含有n-1条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。
广度优先生成树
深度优先生成树
🎈对于一个带权连通无向图G =(V,E),生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。设R为G的所有生成树的集合,若T为R中边的权值之和最小的生成树,则T称为G的最小生成树(Minimum-Spanning-Tree, MST)。
- 最小生成树可能有多个,但边的权值之和总是唯一且最小的
- 最小生成树的边数=顶点数–1。砍掉一条则不连通,增加一条边则会出现回路
- 当图中各边权值不相等时,最小生成树唯一
- 如果一个连通图本身就是一棵树,则其最小生成树就是它本身
- 只有连通图才有生成树,非连通图只有生成森林
✅Prim算法(普里姆)
从某一个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。
时间复杂度: O(IV|2),适合用于边稠密图
实现思想
从Vo开始,总共需要n-1轮处理
每一轮处理: 循环遍历所有个结点,找到lowCost最低的,且还没加入树的顶点。再次循环遍历,更新还没加入的各个顶点的lowCost值
总时间复杂度O(n2),即O(|V|2),每一轮时间复杂度O(2n)
✅Kruskal算法(克鲁斯卡尔)
每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选),直到所有结点都连通。
时间复杂度: O(|E[log2E),适合用于边稀疏图
Kruskal算法的实现思想
共执行e轮,每轮判断两个顶点是否属于同一集合,需要O(log2e)总时间复杂度O(elog2e)
6.4.2 最短路径
✅BFS算法
🎈BFS可以用来求无权图的单源最短路径
代码实现:
//求顶点u到其他顶点的最短路径
void BFS_MIN_Distance(Graph G,int u){
//d[i]表示从u到i结点的最短路径
for(i=0;i<G.vexnum;++1){
d[i]=∞; //初始化路径长度
path[i]=-1; //最短路径从哪个顶点过来
}
d[u]=0;
visited[u]=TRUE;
EnQueue(Q,u);
while(!isEmpty(Q)){ //BFS算法主过程
DeQueue(Q,u); //队头元素u出队
for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w))
if(!visited[w]){ //w为u的尚未访问的邻接顶点
d[w]=d[u]+1; //路径长度加1
path[w]=u; //最短路径应从u到w
visited[w]=TRUE; //设已访问标记
EnQueue(Q,w); //顶点w入队
}
}
}
✅Dijkstra算法(迪杰斯特拉)
🎈用于求带权有向图中某个源点到其余各个顶点的最短路径
如何使用数组信息?
V0到V2的最短(带权)路径长度为:dist[2]=9
通过path[]可知,V0到V2的最短(带权)路径:V2<–V1<–V4<-- V0
时间复杂度: O(n2)即O(|V|2)
Dijkstra算法不适用于有负权值的带权图
✅Floyd算法(弗洛伊德)
🎈可用于求单源最短路径算法来解决每对顶点之间的最短路径问题
对于n个顶点的图G,求任意一对顶点Vi->Vj之间的最短路径可分为如下几个阶段:
- 初始:不允许在其他顶点中转,最短路径是?
- 0:若允许在V0中转,最短路径是?
- 1∶若允许在V0、V1中转,最短路径是?
- 2︰若允许在V0、V1、V2中转,最短路径是?
… - n-1∶若允许在V0、V1、V2 … Vn-1中转,最短路径是?
下面根据一个实例来演示:
核心代码实现:
for (int k=0;k<n;k++){ //考虑以VK作为中转点
for (int i=0;i<n;i++){ //遍历整个矩阵,i为行号,j为列号
for (int j=0;j<n;j++){
if (A[i][j]>A[i][k]+A[k][j]){ //以VK为中转点的路径更短
A[i][j]=A[i][k]+A[k][j]; //更新最短路径长度
path[i][j]=k; //中转点
}
}
}
}
注: Floyd算法允许图中有带负权值的边,但不允许有包含带负权值的边组成的回路,Floyd算法同样适用于带权无向图
时间复杂度:O(|V|3)
小结:
BFS算法 | Dijkstra算法 | Floyd算法 | |
---|---|---|---|
无权图 | √ | √ | √ |
带权图 | × | √ | √ |
带负权值的图 | × | × | √ |
带负权回路的图 | × | × | × |
时间复杂度 | O(V2) 或O(V+E) | O(V2) | O(V3) |
通常用于 | 求无权图的单源最短路径 | 求带权图的单源最短路径 | 求带权图中各顶点间的最短路径 |
6.4.3 有向无环图描述表达式
🎈若一个有向图中不存在环,则称为有向无环图,简称DAG图(Directed Acyclic Graph)
步骤:
- Step 1:把各个操作数不重复地排成一排
- Step 2:标出各个运算符的生效顺序(先后顺序有点出入无所谓)
- Step 3:按顺序加入运算符,注意“分层”
- Step 4:从底向上逐层检查同层的运算符是否可以合体
6.4.4 拓扑排序
✅AOV网
AOV网(Activity on Vertex Network,用顶点表示活动的网):用DAG图(有向无环图)表示一个工程。顶点表示活动,有向边 <Vi,Vj> 表示活动 Vi 必须先于活动 Vj 进行
✅拓扑排序
拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:
- 每个顶点出现且只出现一次
- 若顶点A在序列中排在顶点B的前面,则在图中不存在从顶点B到顶点A的路径
- 每个AOV网都有一个或多个拓扑排序序列
拓扑排序的过程
- ①从AOV图中选择一个入度为0(即没有前驱) 的顶点并输出
- ②然后删除此顶点,并删去以此为起点的有向边(删除出度)
- ③重复以上步骤,直到所有的顶点都输出或者找不到入度为0(无前驱)的点
逆拓扑排序的过程
- 从AOV网中选择一个没有后继(出度为0) 的顶点并输出
- 从网中删除该顶点和所有以它为终点的有向边
- 重复①和②直到当前的AOV 网为空
采用邻接表存储时拓扑排序的时间复杂度为O(|V| + |E|),采用邻接矩阵存储时拓扑排序的时间复杂度为(|V|2)
6.4.5 关键路径
🎈在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网(Activity On Edge NetWork)
✅概念
开始顶点(源点):仅有一个入度为0的顶点——工程开始
结束顶点(汇点):仅有一个出度为0的顶点——工程结束
关键路径:从源点到汇点的所有路径中,具有最大路径长度的路径
关键活动:关键路径上的活动
关键路径的长度:完成整个工程的最短时间
✅参量的定义
事件 vk 的最早发生时间 ve(k)
事件 vk 的最迟发生时间 vl(k)
活动 ai 的最早发生时间 e(i)
活动 ai 的最迟发生时间 l(i)
一个活动 ai 的最迟开始事件 l(i) 和其最早开始时间 e(i) 的差额 d(i) 时间余量
时间余量为0的活动就是关键路径
✅求关键路径的过程
- 对图中顶点进行拓扑排序,在排序过程中按拓扑排序求出每个事件的最早发生时间 ve(i)
- 按逆拓扑排序列求AOE网络中每个事件的最迟发生时间 vl(i)
- 求AOE网中每个活动的最早开始时间e(i)
- 求AOE网中每个活动的最迟开始时间l(i)
- 找出 e(i)= l(i) 的活动 ai,即为关键活动。由关键活动形成的由源点到汇点的每一条路径就是关键路径,关键路径有可能不止一条。
注意:
- 关键路径上所有的活动都是关键活动,它是决定整个工程的关键因素,因此可通过加快关键活动来缩短整个工程的工期。但也不能任意缩短关键活动,因为一旦缩短到一定的程度,该关键活动就可能变成非关键活动。
- 关键路径并不唯一,且对于有几条关键路径的网,只提高一条关键路径上的关键活动并不能缩短整个工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的