目录
一、图的性质
1、易错术语
1.1 <v,w>,v到w的弧,v为弧尾,w为弧头
1.2 简单图:不存在重复的边,不存在顶点到自身的边
1.3 完全图:无向:任意两个顶点之间都存在边,有n(n-1)/2条边;有向:任意两个顶点间存在方向相反的弧,有n(n-1)条弧
1.4 生成子图:A是B的子图,且B的顶点都在A中,边可以不全在
1.5 连通图:任意两个顶点都是连通的
1.6 连通分量:无向图中的极大连通子图(极大连通子图:子图连通并包含尽可能多的边)
1.7 强连通图:任意一对顶点都是强连通的(正向+逆向都有)
1.8 极大强连通分量:有向图的极大连通子图
1.9 生成树:包含全部顶点的极小连通子图
1.10 顶点间的路径:顶点序列
1.11 路径长度:路径上的边的数目
1.12 简单路径:在路径序列中,顶点不重复出现的路径
1.13 简单回路:除首尾顶点,其余顶点不重复
1.14 有向树:一个顶点的入度为0,其余顶点的入度为1的有向图
2、图的性质
2.1 若顶点数为n,则生成树含n-1条边
2.2 边数总结
n为顶点 | min | max |
无向图非连通 | 0 | |
无向图连通 | n-1(一个图有n个顶点,若边数小于n-1,则此图必是非连通图) | |
非强连通 | 0 | (n-2)(n-1)+1 (n-1个顶点的强连通图+一条边+一个顶点) |
强连通 | n(环) |
二、图的存储
1、邻接矩阵法
1.1、存储结构
#define MaxVertexNum 100
typedef char VertexType;
#define INFINITY //带权
typedef int EdgeType;
typedef struct {
VertexType vex[MaxVertexNum]; //顶点表
EdgeType edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexnum, arcnum; //图的当前顶点数和边数
}MGraph;
空间复杂度为O(n^2),n为顶点数
1.2、特点
①无向图的邻接矩阵一定是个对称矩阵,在实际存储邻接矩阵时只需要存储上/下三角矩阵的元素
②对于无向图,第i行/列非零元素个数是顶点i的度
③对于有向图,第i行非零元素个数代表出度,第i列非零元素的个数是入度
④稠密图适合采用邻接矩阵的存储表示
⑤A^n的元素A^n[i][j]等于由顶点i到顶点j的长度为n的路径的数目
2、邻接表法
2.1、存储结构
#define MaxVertexNum 100 //顶点数目最大值
typedef char VertexType;
#define INFINITY //带权
typedef int EdgeType;
//边表结点
typedef struct ArcNode {
int adjvex; //弧所指向的顶点的编号
struct ArcNode* nextarc;
}ArcNode;
//顶点表结点
typedef struct VNode {
VertexType data; //顶点信息
ArcNode* firstarc; //指向第一条依附该顶点的弧的信息
}VNode,AdjList[MaxVertexNum];
//类型
typedef struct {
AdjList vertices; //邻接表
int vexnum, arcnum;
}ALGraph;
2.2、特点
①G若为无向图,所需的存储空间为O(V+2E),若G为有向图,存储空间为O(V+E)。
3、邻接矩阵和邻接表适用性差异
①对于稀疏图,采用邻接表表示极大节省了空间
②给定顶点,查找所有邻边:邻接表很方便,读取它的邻接表即可,但邻矩阵花费时间为O(n)
③确定给定两个顶点是否存在边:邻接矩阵立刻可查到,但邻接表中需要在相应结点对应的边表中查找另一结点
④求度:无向图邻接表中,边表结点的个数
⑤求出度:在有向图邻接表中,边表结点个数
⑥求入度:遍历全部邻接表,统计邻接点(adjvex)域为x的边表结点个数
4、十字链表
有向图的链式存储结构
弧结点 | 顶点结点 | |||||||
tailvex | headvex | hlink | tlink | info | data | firstin | firstout | |
弧尾顶点编号 | 弧头编号 | 弧头相同的下一条弧 | 弧尾相同的下一条弧 | 该顶点作为弧头的第一条弧 | 该顶点作为弧尾的第一条弧 |
图的十字链表是不唯一的,但一个十字链表表示唯一确定一个图
5、邻接多重表
无向图的链式存储结构
弧结点 | 顶点结点 | ||||||
ivex | ilink | jvex | jlink | info | data | firstedge | |
该边依附的顶点编号 | 指向下一条依附于ivex的边 | 该边依附的顶点编号 | 指向下一条依附于jvex的边 | 第一条依附于该顶点的边 |
6、图的存储总结
邻接矩阵 | 邻接表 | 十字链表 | 邻接多重表 | |
空间复杂度 | O(V^2),V为顶点数 | 无向图:O(V+2E),有向图:O(V+E) | O(V+E) | O(V+E) |
找相邻边 | 遍历行/列 | 找有向图的入度遍历整个邻接表 | 方便 | 方便 |
删除边或顶点 | 边-方便 顶点-移动大量数据 | 都不方便 | 方便 | 方便 |
适用于 | 稠密图 | 稀疏图+其他 | 只能存有向图 | 只能存有向图 |
表示方式 | 唯一 | 不唯一 | 不唯一 | 不唯一 |
7、图的基本操作
三、图的遍历
注:所有顶点访问一次且仅访问一次
1、广度优先搜索
1.1、思想
类似于二叉树层次遍历,因此要借助辅助队列。同Dijkstra单源最短路径算法和Prim最小生成树算法。对于邻接矩阵得到的序列是唯一的,对于邻接表得到的序列不唯一
1.2、邻接表实现-代码
#define MaxVertexNum 100 //顶点数目最大值
typedef char VertexType;
#define INFINITY //带权
typedef int EdgeType;
typedef struct {
VertexType vex[MaxVertexNum]; //顶点表
EdgeType edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexnum, arcnum; //图的当前顶点数和边数
}MGraph;
//边表结点
typedef struct ArcNode {
int adjvex; //弧所指向的顶点的编号
struct ArcNode* nextarc;
}ArcNode;
typedef struct VNode {
VertexType data; //顶点信息
ArcNode* firstarc; //指向第一条依附该顶点的弧的信息
}VNode,AdjList[MaxVertexNum];
//类型
typedef struct {
AdjList vertices; //邻接表
int vexnum, arcnum;
}ALGraph;
bool visited[MaxVertexNum];
void BFSTraverse(ALGraph G) {
for (int i = 0; i < G.vexnum; i++) //初始化标记数组
visited[i] = false;
InitQueue(Q);
for (int i = 0; i < G.vexnum; i++) {
if (!visited[i]) //如果该结点没有被访问过,进行访问
BFS(G, i);
}
}
void BFS(ALGraph G, int i) {
visite(i);
visited[i] = true;
Enqueue(Q, i);
while (!isEmpty(Q)) {
DeQueue(Q,v);//队首顶点v出队
ArcNode* p;
for (p = G.vertices[i].firstarc; p; p = p->nextarc) { //检测v的所有邻接点
int w = p->adjvex; // 弧所指向的顶点的编号
if (visited[w] == false) { //没被访问过
visit(w);
visited[w] = true;
EnQueue(Q, w);
}
}
}
}
每个顶点需入队一次,时间复杂度为O(V),在搜索每个顶点的邻接表时,每条边至少访问一次,时间复杂度为O(E),总为O(V+E) 空间复杂度:O(V)
1.3、邻接矩阵实现-代码
#define MaxVertexNum 100
typedef char VertexType;
#define INFINITY //带权
typedef int EdgeType;
typedef struct {
VertexType vex[MaxVertexNum]; //顶点表
EdgeType edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexnum, arcnum; //图的当前顶点数和边数
}MGraph;
bool visited[MaxVertexNum];
void BFSTraverse(MGraph G) {
for (int i = 0; i < G.vexnum; i++)
visited[i] = false;
InitQueue(Q);
for (int i = 0; i < G.vexnum; i++)
if (!visited[i])
BFS(G, i);
}
void BFS(MGraph G, int i) {
visit(i);
visited[i] = true;
Enqueue(Q,i);
while (!isEmpty(Q)) {
Dequeue(Q, v);
for (int w = 0; w < G.vexnum; w++) { //遍历v的所有顶点
if (visited[w] == false && G.edge[v][w] == 1) { //w没被访问过且v,w之间存在边
visit(w);
visited[w] = true;
Enqueue(Q, w);
}
}
}
}
查找每个顶点的邻接点所需的时间为O(V),总为O(V+V^2) 空间复杂度:O(V)
1.4、BFS求单源最短路径-非带权图
void BFS_MIN_DISTANCE(Graph G, int u) {
for (int i = 0; i < G.vexnum; i++)
d[i] = -999; //d[i]表示u到i结点的最短路径
visited[u] = true;
d[u] = 0;
Enqueue(Q, u);
while (!isEmpty(Q)) {
DeQueue(Q,u);//队首顶点v出队
for (w = G.FirstNeighbor(G, u); w >= 0; w = NextNeighbor(G, u, w)) { //检测v的所有邻接点
if (!visited[w]) { //没被访问过
visited[w] = true;
d[w] = d[u] + 1;
EnQueue(Q, w);
}
}
}
}
1.5、BFS求广度优先生成树
注意:邻接矩阵遍历所生成的生成树是唯一的,但邻接表所生成的生成树不唯一
2、深度优先搜索
2.1、思想
类似于树的先序遍历;借助栈,若在遍历过程中欲访问结点已在栈中,则有环。对于邻接矩阵得到的序列是唯一的,对于邻接表得到的序列不唯一
2.2、邻接表实现-代码
#define MaxVertexNum 100 //顶点数目最大值
typedef char VertexType;
#define INFINITY //带权
typedef int EdgeType;
typedef struct {
VertexType vex[MaxVertexNum]; //顶点表
EdgeType edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexnum, arcnum; //图的当前顶点数和边数
}MGraph;
//边表结点
typedef struct ArcNode {
int adjvex; //弧所指向的顶点的编号
struct ArcNode* nextarc;
}ArcNode;
typedef struct VNode {
VertexType data; //顶点信息
ArcNode* firstarc; //指向第一条依附该顶点的弧的信息
}VNode,AdjList[MaxVertexNum];
//类型
typedef struct {
AdjList vertices; //邻接表
int vexnum, arcnum;
}ALGraph;
bool visited[MaxVertexNum];
void DFSTraverse(ALGraph G) {
for (int i = 0; i < G.vexnum; i++)
visited[i] = false;
for (int i = 0; i < G.vexnum; i++)
if (!visited[i])
DFS(G, i);
}
void DFS(ALGraph G, int i) {
visit(i);
visited[i] = true;
ArcNode* p;
for (p = G.vertices[i].firstarc; p; p = p->nextarc) { //检测v的所有邻接点
int w = p->adjvex; // 弧所指向的顶点的编号
if (visited[w] == false) { //没被访问过
DFS(G, w);
}
}
}
时间复杂度:O(V+E) 空间复杂度:O(V)
2.3、邻接矩阵实现-代码
#define MaxVertexNum 100
typedef char VertexType;
#define INFINITY //带权
typedef int EdgeType;
typedef struct {
VertexType vex[MaxVertexNum]; //顶点表
EdgeType edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexnum, arcnum; //图的当前顶点数和边数
bool visited[MaxVertexNum];
void DFSTraverse(MGraph G) {
for (int i = 0; i < G.vexnum; i++)
visited[i] = false;
for (int i = 0; i < G.vexnum; i++)
if (!visited[i])
DFS(G, i);
}
void DFS(MGraph G, int i) {
visit(i);
visited[i] = true;
for (int j = 0; j < G.vexnum; j++) {
if (visited[i] == false && G.edge[i][j] == 1)
DFS(G, j);
}
}
时间复杂度:O(V^2) 空间复杂度:O(V)
2.4、深度优先的生成树和生成森林
对于连通图调用DFS才能产生深度优先生成树,否则是优先生成森林
邻接矩阵遍历所生成的生成树是唯一的,但邻接表所生成的生成树不唯一
3、图的遍历与图的连通性
图的遍历算法可以判断图的连通性
在BFSTraverse和DFSTraverse中,对于无向图,调用B/DFS的次数等于该图的连通分量数。
但对于有向图不然,一个连通的有向图分别为强连通和非强连通,它的连通子图也分为强连通分量和非强连通分量,非强连通分量一次调用B/DFS无法访问该连通分量的所有结点
但并非任意非强连通图必须2次及以上遍历才可以访问所有结点,如①->②->③
四、图的应用
1、最小生成树
1.1、定义及性质
①最小生成树:权值之和最小的生成树(一个连通图的生成树包含图的所有顶点,并且只含尽可能少的边)
②若无向连通图的边数比顶点数少1,那么它本身就是一棵最小生成树
③最小生成树中所有边的权值之和最小,但不能确保任意两个顶点之间的路径是最短路径
④贪心算法-构造最小生成树
GENERIC_MTS(G){
T=NULL;
while(T未形成一棵生成树){
找到一条最小代价边(u,v)并加入T后不产生回路;
T=T∪(u,v);
}
1.2、Prim算法
类似于找最短路径
void Prim(G,T){
T=空;
U={w}; //任意加一个顶点w
while((V-U)!=空){ //当所有顶点都不全在U中
T=T∪{(u,v)}; //{u,v}是当前集合顶点的权值最小的边
U=U∪{v};
}
时间复杂度:O(V^2) ,适用于求解边稠密的图的最小生成树
1.3、Kruskal算法
void Kruskal(G,T){
T=V; //仅含顶点
numS=n; //连通分量数
while((numS>1){
从边集合中取出权值最小的边(v,u)
if(v,u属于T中不同的连通分量)
T=T∪{(u,v)}; //加入生成树
numS--;
}
时间复杂度: ,适合于边稀疏而顶点较多的图
2、最短路径
2.1、Dijkstra算法
①求图中某一顶点到其他各顶点的最短路径
②在集合S中加入源点v,每次加入一个新顶点,同时修改v到V-S中顶点的最短路径长度
③邻接矩阵表示时,时间复杂度:O(V^2)
④Prim和Dijkstral算法的差异:
目的不同:D目的是构建单源点的最短路径树,P是构建最小生成树
算法思路稍有不同:P是从一个点开始,每次选择权值最小的边,将其连接到已构建的生成树上,直至所有顶点都已加入;D是每次找出到源点距离最近且未归入集合的点,并把它归入集合,同时以这个点为基础更新从源点到其他所有顶点的距离。
适用图不一样:P适用于带权无向图;D适用于带权有向图或带权无向图
时间复杂度不同
⑤边上带有负权值时,Dijkstra算法不适用
2.2、Floyd算法
①求图中所有顶点之间的最短路径
②先初始化一个矩阵,内容为每个顶点之间的直接路径长度,经过递推产生后序矩阵,如矩阵A0表示以v0结点作为中间顶点,所更新得到的矩阵,依次类推
③时间复杂度:O(V^3) 空间复杂度:O(V^2)
④Floyd算法允许图中带有负权值的边,但不允许有包含带负权值的边组成的贿赂
⑤适用于带权无向图
2.3 总结
BFS | Dijkstra | Floyd | |
用途 | 求单源最短路径 | 求单源最短路径 | 求各顶点之间的最短路径 |
无权图 | 适用 | 适用 | 适用 |
带权图 | 不适用 | 适用 | 适用 |
带负权值的图 | 不适用 | 不适用 | 适用 |
带负权值回路的图 | 不适用 | 不适用 | 不适用 |
时间复杂度 | O(V^2)或O(V+E) | O(V^2) | O(V^3) |
3、有向无环图描述表达式
描述含有公共子式的工具
4、拓扑排序
4.1、性质及代码
①AOV网:有向无环图表示工程,顶点表示活动,有向边表示关系
②拓扑排序:对有向无环图的顶点的排序,使得若存在一条从顶点A到顶点B的路径,则在排序中B出现在A的后面,每个AOV网都有一个或多个拓扑排序序列
③若网中不存在无前驱的顶点,说明有向图中存在环
bool TopologicalSort(ALGraph G) {
InitStack(S);
int i;
for (i = 0; i < G.vexnum; i++)
if (indegree[i] == 0) //将入度为0的顶点入栈
Push(S, i);
int count = 0; //已输出的顶点数
while (!isEmpty(S)) {
Pop(S, i);
print[count++] = i; //输出顶点
ArcNode* p;
for (p = G.vertices[i].firstarc; p; p = p->nextarc) {
//将所有i指向的顶点的入度都-1,并将入度减为0的顶点压入栈
int v = p->adjvex;//p的下一个顶点
if (!(--indegree[v]))
Push(S, v);
}
}
if (count < G.vexnum)
return false;
else return true;
}
邻接表存储。时间复杂度:O(V+E)
邻接矩阵存储,时间复杂度:O(V^2)
4.2、DFS实现拓扑排序
bool visited[MaxVertexNum];
void DFSTraverse(ALGraph G) {
for (int i = 0; i < G.vexnum; i++)
visited[i] = false;
int time = 0;
for (int v = 0; v < G.vexnum; v++)
if (!visited[v])
DFS(G, v);
}
void DFS(ALGraph G, int v) {
visited[v] = true;
visit(v);
for(w = G.FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w)) { //检测v的所有邻接点
if (!visited[w])
DFS(G, w);
time = time + 1;
finishTime[v] = time;
}
5、关键路径
5.1、相关定义
①AOE网:对带权有向图,顶点表示事件,有向边表示活动,权值表示开销
②只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始
③只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生
④关键路径:最大路径长度的路径
⑤关键活动:关键路径上的活动
5.2、求关键路径的步骤
①求出事件Vk的最早发生时间Ve(k) -从源点V1到顶点Vk的最长路径长度
Ve(源点)=0; Ve(k)=max{前一个顶点的最长路径长度+两个顶点之间的权值}
②求出事件Vk的最迟发生时间Vi(k)
Vi(汇点)=Ve(汇点); Vi(k)=min{后继顶点的最迟发生时间-两个顶点之间的权值}
③活动ai最早开始时间e(i) -该活动弧的起点所表示的事件的最早发生事件
e(i)=Ve(k);
④活动ai的最迟开始时间 l(i) -活动弧的终点所表示事件的最迟发生事件与该活动所需时间之差
⑤一个活动ai的最迟开始时间和最早开始时间的差 -活动完成的时间余量
当余量=0时,活动ai为关键活动
5.3、缩短工期
①不能任意缩短关键活动
②关键路径不唯一,对于有几条关键路径的网,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包含在所有关键路径上的关键活动才能缩短工期
6、采用不同存储结构时各种图算法的时间复杂度
Dijkstra | Floyd | Prim | Kruskal | DFS | BFS | 拓扑排序 | 关键路径 | |
邻接矩阵 | O(n^2) | O(n^3) | O(n^2) | O(n^2) | O(n^2) | O(n^2) | O(n^2) | |
邻接表 | O(eloge) | O(n+e) | O(n+e) | O(n+e) | O(n+e) |