1. 图的定义和基本术语
1.1 图的定义
1.2 无向图和有向图
1.3 简单图和多重图
1.4 顶点的度、入度、出度
1.5 顶点-顶点的关系描述
1.6 连通图和强连通图
1.7 子图、生成子图
1.8 连通分量(无向图)
1.9 强连通分量(有向图)
1.10 生成树、生成森林
1.11 边的权、带权图/网
1.12 无向完全图、有向完全图、稀疏图、稠密图
2. 图的存储
2.1 邻接矩阵法
2.1.1 邻接矩阵存储带权图(网)
2.1.2 邻接矩阵的性能分析
2.1.3 邻接矩阵的性质
以此类推,可以得到A2 的矩阵。
A3 也是同样的道理,则表示A[i][j] 由 i 到 j 路径长度为3的路径数目。
2.2 邻接表法(顺序+链式存储)
这种存储图的方法其实跟树的孩子表示法有点相似。
邻接矩阵存储无向图时,一条边会有两个顶点都记录着,所以边结点的数量是边的两倍,即2 * |E|。算各个结点的度也比较容易,直接看各个结点的指针区域没有连着几个结点。
当存储的是有向图时,边结点的数量就是边数,即|E|,出度就是对应结点指针区域后面连着的结点数量,而入度就得把整个邻接表遍历一遍才能知道。
2.3 十字链表(存储有向图)
十字链表找某个结点所有的边(出边和入边)都是很方便的。
不会考代码。
2.4 邻接多重表(无向图)
3. 图的基本操作
3.1 判断图中是否存在某条边
有向图类似:
3.2 列出与某个顶点邻接的边
因为邻接矩阵的一行或一列有V个元素,所以以上邻接矩阵的操作的时间复杂度是O(|V|)。
3.3 插入顶点
3.4 删除顶点
3.5 添加边
3.6 求第一个邻接点(常用到)
考试可以直接调用这个接口。
3.7 找下一个邻接点(常用到)
考试可以直接调用这个接口。
3.8 获取对应边的权值或设置权值
4. 图的遍历
4.1 广度优先遍历(BFS)
图的广度优先遍历有点类似于树的广度优先遍历(层序遍历)。
4.1.1 代码实现
4.1.2 广度优先遍历序列
4.1.3 遍历序列的可变性
4.1.4 算法存在的问题
4.1.5 BFS算法(最终版)
4.1.6 复杂度分析
4.1.7 广度优先生成树
4.1.8 广度优先生成森林
4.2 深度优先遍历(DFS)
4.2.1 代码实现
4.2.2 算法实现的问题
4.2.3 DFS算法(最终版)
4.2.4 复杂度分析
4.2.5 遍历序列的可变性
4.2.6 深度优先生成树
4.2.7 图的遍历和图的连通性
5. 最小生成树
5.1 概念
5.2 Prim算法(普里姆)
从P城这个顶点开始构建生成树:
整个过程就是上面这样子,当然按照这个算法,一个图从一个顶点去生成树也可能有多个最小生成树。
例如,也可以是这样,最小代价是一样的,就是树不同而已。
从“农场”顶点开始构建生成树:
最小代价还是一样。
5.2.1 实现思想
5.3 Kruskal算法(克鲁斯卡尔)
5.3.1 实现思想
6. 最短路径
6.1 BFS求无权图的单源最短路径
举个例子,以顶点2作为起点:
6.2 Dijkstra算法(迪杰斯特拉)
Dijkstra算法有向图和无向图都可以用。
这样我们研究有向图:
6.2.1 时间复杂度
循环遍历dist数组,找出最小值,所需时间复杂度是O(n);检查所有邻接自Vi ,如果是用邻接矩阵存储图的话,则需要扫描Vi 对应的一整行,即O(n),所以总的复杂度是O((n+n)*(n-1)),即O(n2 )。如果是用邻接表存储图的话,Vi 对应的链表最多有n-1个元素,所以总的时间复杂度是O(((n-1)+n) * (n-1)),同样是O(n2 )。
Prim算法实现思想跟Dijkstra算法实现的思想是很类似的。只不过Prim算法的lowCost数组是指,其他没有加入树的顶点要加入到树的代价;而Dijkstra算法的dist数组是指当前顶点到其他顶点的最短路径。而它们的时间复杂度是一样的。
6.2.2 Dijkstra算法不适用于带负值的带权图
如果这个图采用Dijkstra算法,v0到v2的最短路径是7。
6.3 Floyd算法
6.3.1 核心代码和复杂度
因为有三层循环,每个for循环都是n,所有时间复杂度是O(n3 ),即O(|V|3 )。最外一层循环是用来考虑是否采用Vk 作为中转点的。
而这个算法需要两个二维数组A和path,所有空间复杂度是O(2*n2 ),即O(|V|2 )。
举个例子:
6.3.2 可用于负权值带权图
6.3.3 不能解决的问题
7. 有向无环图
7.1 描述表达式
树是自顶向下的,所以树可以算作一种有向无环图,就是把上面的指针指向同个结点也还是有向无环图。
相同的子树可以合并,从而减少结点数量,节省空间。
所以最后形成的有向无环图是这样子的:
曾经真题也出现过这样的题:
7.1.1 总结规律
再往上层已经没有可以合并的了,所以最后的有向无环图就是以上所示。
7.2 拓扑排序
有两个选择,我们先选择“准备厨具”:
到这里之后,就依次执行下去,我们可以总结出一个规律:
执行到这里时,已经没办法继续执行下去了。
7.2.1 核心代码
拓扑排序,我们先处理的是入度为0的顶点。
7.2.2 用DFS实现拓扑排序
对于有向无环图G中的任意结点u、v,它们之间的关系必然是一下三种之一:
-
假设结点u是结点v的祖先,则在调用DFS访问u的过程中,必然会在这个过程结束之前递归地对 v 调用DFS访问,即v的DFS函数结束时间先于u的DFS结束时间。从而可以考虑在DFS调用过程中设定一个时间标记,在DFS调用结束时,对各结点计时。因此,祖先的结束时间必然大于子孙的结束时间。
-
若u是结点v的子孙,则v为u的祖先,按上述思路,v 的结束时间大于u的结束时间。
-
若u和v没有关系,则u和v在拓扑序列的关系任意。
从而按结束时间从大到小,可以得到一个拓扑序列。
下面给出利用DFS求各结点结束时间的代码。至于拓扑序列,将结束时间从大到小排序即可得到(实际上和深度优先遍历算法完全相同,只不过加入了time变量)。
bool visited[MAX_VERTEX_NUM]; //访问标记数组
void DFSTraverse(Graph G){
for(v=0;v<G.vexnum;++v){
visited[v]=false; //初始化访问标记数组
}
time=0;
for(v=0;v<G.vexnum;++v){
if(!visited[v]){
DFS(G,v);
}
}
}
void DFS(Graph G,int v){
visited[v]=true;
visit(v);
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){
if(!visited[w]){ //w为v的尚未访问的邻接顶点
DFS(G,w);
}
}
time=time+1;
finishTime[v]=time;
}
7.3 逆拓扑排序
拓扑排序是先处理入度为0的顶点,而逆拓扑排序是先处理出度为0的结点。
这里也有多种选择,所以形成的逆拓扑排序序列也可以有多个。
7.3.1 核心代码
//这里采用的是邻接矩阵法
bool ni-TopologicalSort(Graph G){
InitStack(S); //初始化栈,存储出度为0的顶点,这里是假设已经初始化好outdegree数组(记录当前顶点的出度)和print数组的
for(int i=0;i<G.vexnum;i++){
if(outdegree[i]==0)
Push(S,i); //将所有出度为0的顶点进栈
}
int count=0; //记录当前已经输出的顶点数
while(!IsEmpty(S)){ //栈不空则存在出度为0的顶点
Pop(S,i); //栈顶元素出栈
print[count++]=i;//将输出顶点保存在print数组中
for(int j=0;j<G.vexnum;j++){ //这里我采用的是邻接矩阵的存储方式
//将所有指向i的顶点的出度减1,并且将出度减为0的顶点压入栈S
if(A[j][i]==1){ //说明j->i有出边
if(!(--outdegree[j])){
Push(S,j); //出度为0,则入栈
}
}
}
}
if(count<G.vexnum){ //count小于顶点数
return false; //排序失败,有向图中有回路
}else{
return true; //逆拓扑排序成功
}
}
7.3.2 用DFS实现逆拓扑排序
当出现回路时,可以参考这个博主的链接:
图的逆拓扑排序
7.4 关键路径
7.4.1 求关键路径的步骤
求所有路径的最早发生时间
求所有事件的最迟发生时间
求所有活动的最早发生时间
求所有活动的最迟发生时间
求所有事件的时间余量
求得关键活动和关键路径
文章来源:https://www.toymoban.com/news/detail-428031.html
7.4.2 关键活动、关键路径的特征
文章来源地址https://www.toymoban.com/news/detail-428031.html
到了这里,关于《数据结构》王道 第六章 图的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!