六、图
目录
- 六、图
- 定义及基本术语
- 图的定义
- 有向图以及无向图
- 简单图以及多重图
- 度
- 顶点-顶点间关系
- 连通图、强连通图
- 子图
- 连通分量
- 强连通分量
- 生成树
- 生成森林
- 边的权、带权网/图
- 特殊形态的图
- 图的存储及基本操作
- 邻接矩阵
- 邻接表法
- 十字链表
- 邻接多重表
- 分析对比
- 图的基本操作
- 图的遍历
- 广度优先遍历(BFS)
- 深度优先遍历(DFS)
- 图的应用
- 最小生成树
- 最短路径BFS
- 最短路径Dijkstra
- 最短路径Floyd算法
- 有向无环图
- 拓扑排序
- 关键路径
定义及基本术语
图的定义
有向图以及无向图
简单图以及多重图
度
顶点-顶点间关系
连通图、强连通图
子图
(有向图也一样)
连通分量
强连通分量
生成树
生成森林
边的权、带权网/图
特殊形态的图
总结:
图的存储及基本操作
邻接矩阵
#define MaxVertexNum 100//顶点数目的最大值
typedef struct
{
char Vex[MaxVertexNum];//顶点表 (可存更复杂的信息)
int Edge[MaxVertexNum][MaxVertexNum];//邻接矩阵,边表(可以用bool型或枚举型变量表示边)
int vexnum, arcnum;//图的当前顶点数和边数/弧数
}MGraph;
存储带权图(网):
对角线处可以填0或∞
空间复杂度为O(|V|2)只和顶点数相关,和实际的边数无关,适合用于存储稠密图
对于无向图,邻接矩阵是对称矩阵,可以进行对称矩阵的存储压缩,存入一维数组中(只存储上三角区/下三角区)
性质:
邻接表法
邻接矩阵是用数组实现的顺序存储,空间复杂度高,不适合存储稀疏图
而邻接表法使用顺序+链式存储的方式,表示方式并不唯一(与树的孩子表示法相似)
//邻接表
typedef char VertexType;//顶点的数据类型
//“边/弧”
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;
与邻接矩阵对比:
十字链表
用邻接矩阵以及邻接表存储有向图时,都有所缺陷:
使用十字链表存储有向图(不能用于无向图):
空间复杂度为:O(|V|+|E|)
顺着绿色路线能找到顶点所有的出边
顺着橙色路线能找到顶点所有的入边
邻接多重表
用邻接矩阵以及邻接表存储无向图时,都有所缺陷:
用邻接多重表存储无向图(不能用于有向图):
空间复杂度:O(|V|+|E|)
删除边、删除节点等操作很方便
分析对比
图的基本操作
主要基于图的邻接矩阵以及邻接表
**Adjacent(G,x,y):**判断图G是否存在边<x, y>或(x, y)。
邻接矩阵:O(1) 邻接表:O(1)~O(|V|)
**Neighbors(G,x):**列出图G中与结点x邻接的边。
邻接矩阵:O(|V|) 邻接表:出边:O(1)~O(|V|) 入边:O(|E|)
**InsertVertex(G,x):**在图G中插入顶点x
邻接矩阵:O(1) 邻接表:O(1)
**DeleteVertex(G,x):**从图G中删除节点x
邻接矩阵:O(|V|) 邻接表:出边:O(1)~O(|V|) 入边:O(|E|)
**AddEdge(G,x,y):**若无向边(x, y)或有向边<x, y>不存在,则向图G中添加该边。
邻接矩阵:O(1) 邻接表:O(1)
**RemoveEdge(G,x,y):**若无向边(x, y)或有向边<x, y>存在,则从图G中删除该边。
邻接矩阵:O(1) 邻接表:O(1)~O(|V|)
**FirstNeighbor(G,x):**求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1。
邻接矩阵:O(1)~O(|V|) 邻接表:出边:O(1) 入边:O(1)~O(|E|)
**NextNeighbor(G,x,y):**假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1。
邻接矩阵:O(1)~O(|V|) 邻接表:O(1)
图的遍历
广度优先遍历(BFS)
实现思路:
#define MAX_VERTEX_NUM 100//顶点数目的最大值
bool visited[MAX_VERTEX_NUM];//访问标记数组
void BFSTraverse(Graph G) //对图G进行广度优先遍历
{
for (int i = 0; i < G.vexnum; ++i)
visited[i] = false;//访问标记数组初始化
InitQueue(Q);//初始化辅助队列Q
for (int 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 >= 0; w = NextNeighbor(G, v, w))
{
//检测v所有邻接点
if (!visited[w])//w为v的尚未访问的邻接顶点
{
visit(w);//访问顶点w
visited[w] = true;//对w做已访问标记
EnQueue(Q, w);//顶点w入队列
}
}
}
}
遍历序列是具有可变性的
对于无向图,调用BFS函数的次数=连通分量数
复杂度分析:
广度优先生成树(若是非连通图,则得到广度优先生成森林)
利用广度优先生成的树,高度是最小的(因为按最短路径)
应用:解决非带权图的单源最短路径问题
//解决非带权图的单源最短路径问题
void BFS_MIN_Distance(Graph G, int u)
{
//d[i]表示从u到i结点的最短路径
for (int i = 0; i < G.vexnum; ++i)
{
d[i] = 0x3f3f3f3f;//无穷大,初始化路径长度
}
visited[u] = true;
d[u] = 0;
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的尚未访问的邻接顶点
{
visited[w] = true;//设已访问标记
d[w] = d[u] + 1;//路径长度加1
EnQueue(Q, w);//顶点w入队
}
}
}
}
深度优先遍历(DFS)
类似于树的先根遍历,使用函数调用栈,递归实现
#define MAX_VERTEX_NUM 100//顶点数目的最大值
bool visited[MAX_VERTEX_NUM];//访问标记数组
void DFSTraverse(Graph G)//深度优先遍历图G
{
for (v = 0; v < G.vexnum; ++v)
visited[v] = false;//初始化已访问标记数据
for (v = 0; 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 = NextNeighbor(G, v, w))
{
if (!visited[w])
{
DFS(G, w); // w为v的尚未访问的邻接顶点
}
}
}
复杂度分析:
遍历序列不唯一性:
深度优先生成树:(非连通生成森林)
对于无向图进行BFS/DFS遍历,调用BFS/DFS次数=连通分量数,对于连通图,只调用一次
对于有向图进行BFS/DFS遍历,调用BFS/DFS次数要具体问题具体分析,若起始顶点到其他各顶点都有路径,只调用一次,对于强连通图,从任一结点出发都只需调用一次BFS/DFS
图的应用
最小生成树
连通图的生成树是包含图中全部顶点的一个极小连通子图。若图中顶点数为n,则它的生成树含有n-1条边,对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。
广度优先生成树的高度是小于等于深度优先生成树的高度的。
最小生成树定义:
两种求最小生成树的方法:
Kruskal:
Prim:
最短路径BFS
最短路径问题描述:
利用BFS算法可以求无权图的单源最短路径(无权图可以视作一种特殊的带权图,只是每条边的权值都为1)故各边权值相等时,可以使用BFS算法求解
代码实现:
//解决非带权图的单源最短路径问题
void BFS_MIN_Distance(Graph G, int u)
{
//d[i]表示从u到i结点的最短路径
for (int i = 0; i < G.vexnum; ++i)
{
d[i] = 0x3f3f3f3f;//无穷大,初始化路径长度
path[i]=-1;//记录最短路径从哪个顶点过来
}
visited[u] = true;
d[u] = 0;
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的尚未访问的邻接顶点
{
visited[w] = true;//设已访问标记
d[w] = d[u] + 1;//路径长度加1
path[w]=u;//最短路径应从u到w
EnQueue(Q, w);//顶点w入队
}
}
}
}
时间复杂度:邻接矩阵O(|V|2) 邻接表O(|V|+|E|)
最短路径Dijkstra
dist[ ]记录从源点v0到其他各顶点当前的最短路径长度,它的初态为:若从v0到vi有弧,则dist[i]为弧上的权值,否则置于∞
path[ ]表示从源点到顶点i之间的最短路径的前驱结点。在算法结束时,可根据其值追溯得到源点v0到顶点vi的最短路径。
不适用于有负权值的带权图
用邻接矩阵以及邻接表时间复杂度都为O(|V|2)
最短路径Floyd算法
算法思路:
最终实现:
对于更多结点,若要找到完整路径需要通过path矩阵递归寻找
Floyd算法可以用于负权图,但不能解决带有“负权回路”的图(有负权值的边组成回路)这种图有可能没有最短路径
不同算法对比:
有向无环图
若一个有向图中不存在环,则称为有向无环图,简称DAG图
有向无环图是描述含有公共子式的表达式的有效工具
其表示表达式中顶点中不可能出现重复的操作数
步骤:
表示出来的结果可能不唯一
拓扑排序
AOV网:用DAG图表示一个工程,顶点表示活动,有向边<Vi,Vj>表示活动Vi必须先于活动Vj进行,不能存在环路!
拓扑排序:
实现代码:
#define MaxVertexNum 100//图中顶点数目的最大值
//定义邻接表
typedef char VertexType;//顶点的数据类型
//“边/弧”
typedef struct ArcNode
{
int adjvex;//边/弧指向哪个节点
struct ArcNode* nextarc;//指向下一条弧的指针
//InfoType info;//权值
}ArcNode;
//“顶点”
typedef struct VNode
{
VertexType data;//顶点信息
ArcNode* firstarc;//第一条边/弧
}VNode, AdjList[MaxVertexNum];
//用邻接表存储的图
typedef struct
{
AdjList vertices;
int vexnum, arcnum;
}ALGraph;
//拓扑排序
bool TopologicalSort(Graph G)
{
InitStack(S);//初始化栈,存储入度为0的结点
for (int i = 0; i < G.vexnum; i++)
{
if (indegree[i] == 0)
{
Push(S, i);//将所有入度为0的顶点进栈
}
}
int count = 0;//计数,记录当前已经输出的顶点数
while (!IsEmpty(S))//栈不空,则存在入度为0的顶点
{
Pop(S, i);//栈顶元素出栈
print(count++) = i;//输出顶点i
for (p = G.vertices[i].firstarc; p; p = p->nextarc)
{
//将所有i指向的顶点的入度减1,并且将入度为0的顶点压入栈S
v = p->adjvex;
if (!(--indegree[v]))//若为0
{
Push(S, v);//入栈
}
}
}
if (count < G.vexnum)
{
return false;//排序失败,有向图中有回路
}
else
return true;//拓扑排序成功
}
时间复杂度:邻接表:O(|V|+|E|) 若采用邻接矩阵 O(|V|2)
逆拓扑排序:
也可以用DFS算法实现:
//逆拓扑排序
void DFSTraverse(Graph G)//深度优先遍历图G
{
for (v = 0; v < G.vexnum; ++v)
visited[v] = false;//初始化已访问标记数据
for (v = 0; 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 = NextNeighbor(G, v, w))
{
if (!visited[w])
{
DFS(G, w); // w为v的尚未访问的邻接顶点
}
}
print(v);//输出顶点
}
关键路径
在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网。
性质:
AOE网中仅有一个入度为0的顶点,称为开始顶点(源点),也仅有一个出度为0的顶点,称为结束顶点(汇点)
从源点到汇点的有向路径可能有多条,所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动
完成整个工作的最短时间就是关键路径的长度,若关键活动 不能按时安成为,则整个工程的完成时间就会延长。
几个概念:
求关键路径的步骤:
求事件的最早发生时间:
求事件的最迟发生时间:
求活动的最早发生时间:
求活动的最迟发生时间:
求活动的时间余量:
最终得出关键路径:
特性:
若关键活动耗时增加,则整个工程的工期将增长,缩短关键活动的时间,可以缩短整个工程的工期,当缩短到一定程度时,关键活动可能会变成非关键活动。
可能有多条关键路径,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。
思路总结:
主要参考:王道考研课程
后续会持续更新考研408部分的学习笔记,欢迎关注。
github仓库(含所有相关源码):408数据结构笔记