
文章目录
拓扑排序是图论中处理有向无环图(DAG)的重要算法,用于确定活动执行的合理顺序。本文将详细介绍拓扑排序算法的原理以及拓扑排序的实现。在讲解之前,我们需要先认识了解一下 有向无环图(DAG) 和 AOV 网 (顶点活动图)。
有向无环图:是由顶点和边连接而成的数据结构,图中的边都是有方向的边,无环意味着随意将图中的点提出来,将他们的边组合起来后都是没有环出现的。下图中图一就是有向无环图,图二存在环,不是有向无环图。

下面我们补充两个有向无环图中的概念,入度 和出度
入度和出度都是针对当前的这个顶点来看的
入度 :指向当前顶点的边
出度 :从当前点指出去的边eg:上图中,顶点1的入度为0,出度为2;顶点2的入度为1,出度为2,剩下的顶点同理可得,这里不再详细说明。
AOV网:在有向无环图中,用顶点来表示一个活动,用边来表示活动执行的先后顺序的图结构。下面我们举个例子说明一下,如下图(做法多样,图片仅供参考):

图中每个结点代表一个活动,每条边代表活动的执行顺序。比如:在洗菜前的一个条件肯定是得先有菜,在切菜前肯定得先有厨具和菜。像这样的一个流程图就是一个AOV网,AOV网是很具有实际意义的,在现实生活中的应用还是很广泛的。
拓扑排序介绍
在前两个铺垫的知识点讲完后,接下来我们来认识一下什么是拓扑排序。
拓扑排序(Topological sorting)要解决的问题是如何给一个有向无环图的所有节点排序。简而言之就是找到做一件事情的先后顺序,其中对应的事情就是一个一个的活动。继续拿我们上面的青椒炒肉工程图来进行讲解。
拓扑排序原理
①:确定活动的起点,起点有两个选择,一个是准备厨具,另一个是买菜,这两个活动代表的顶点入度均为0 。这里我们选择准备厨具,准备厨具是腌肉和切菜的前提条件 ,图示变化如下(这里可以将图中准备厨具顶点直接移至下方,方便看到过程变化这里未进行移动):
②:现在我们可选的操作只有买菜,买菜顶点的入度为0 ,注意腌肉还有一个前提条件是洗菜(这里菜指可食用物品总称),腌肉顶点的入度为1 ,图示变化如下:
③:如上图所示,接下来我们可选择进行的活动只有洗菜。同理后续的步骤就不再一一进行演示了。若最开始选择买菜,后续的活动执行流程和上面分析的一样。通过分析发现规律,当顶点的入度为0时,当前顶点便可选,不为0时,不可以进行选择 。
当我们重复上面的操作,一直到最后干饭这个顶点对应的活动被选择,DAG这个图中不存在顶点,我们便完成了本次的拓扑排序。
如何排序呢?通过上面的分析我们不难发现,首先 我们需要找出图中入度为0的顶点,然后依据题目要求,对其进行输出或保存;然后删除与该结点相连的边,重复上面两个操作,直到 DAG 图中没有顶点被边指向。
拓扑排序实现
借助队列,来一次 BFS 即可。具体实现步骤如下:
实验步骤1.初始化:将图中所有入度为0的顶点加入到队列中
2.当队列不为空的时候:
- 拿出对头元素,加入到最终结果中(依据题目而定);
- 删除与该元素相连的边;
- 判断:与该边相连的点,是否入度变为0,如果入度为0,加入到队列中
重复上面 2 步骤,直到队列中不再存在顶点信息。注意在拿到一道算法题时,不可能参数传递为一个图像,一般传递的都是一个二维数组,我们需要自己将题目已知的二维数组转化为图,
建图这个过程,将在下面的例题进行介绍。
接下来我们将具体的实现步骤与实际例题结合起来,便于理解。
拓扑排序例题
例题坐标:
点击跳转拓扑排序例题--课程表
题目描述
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
示例
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
例题解析
不知道AOV网和拓扑排序思想的小伙伴拿到这个题可能脑瓜子嗡嗡的。题目的意思是在学习某某课程前,必须要先完成某某课程。这不就是上述拓扑排序的定义,找到事情执行的先后顺序,根据上文,可以写出代码,后续会对代码进行解释。
代码实现
java
class Solution {
public boolean canFinish(int n, int[][] p) {
int[] in=new int[n];//用于记录元素入度的次数
Queue<Integer> queue=new ArrayDeque<>();//存入当前本次入度为0的全部元素
HashMap<Integer,List<Integer>> map=new HashMap<>();//拓扑建图
for(int i=0;i<p.length;i++){
int a=p[i][0],b=p[i][1];
if(!map.containsKey(b)) map.put(b,new ArrayList<>());
map.get(b).add(a);//建图
in[a]++;//计算入度
}
for(int i=0;i<n;i++){
if(in[i]==0) queue.offer(i);
}
while(!queue.isEmpty()){
int size=queue.size();
while(size-->0){
int cur=queue.poll();
List<Integer> list=map.getOrDefault(cur,new ArrayList<>());
for(int x:list){
in[x]--;
if(in[x]==0) queue.offer(x);
}
}
}
for(int i=0;i<n;i++){
if(in[i]!=0) return false;
}
return true;
}
}
代码解析
代码解析part1
java
int[] in=new int[n];//用于记录元素入度的次数
Queue<Integer> queue=new ArrayDeque<>();//存入当前本次入度为0的全部元素
HashMap<Integer,List<Integer>> map=new HashMap<>();//拓扑建图
for(int i=0;i<p.length;i++){
int a=p[i][0],b=p[i][1];
if(!map.containsKey(b)) map.put(b,new ArrayList<>());
map.get(b).add(a);//建图
in[a]++;//计算入度
}
在实现拓扑排序之前,我们需要先知道拓扑图的信息。已知题目传递的参数为数组,如何将图和数组建立关联呢?这里的关联是,我们需要知道每个顶点四周边的走向,这里我们可以通过HashMap来实现,具体情况为HashMap<Integer,List<Integer>>,其中key表示先需要执行活动顶点对应的 val 值,value 表示 key 顶点指出方向的顶点对应 val 值。拿示例 1 来看,学习 1 课程前必须要学会 0 课程,箭头的指示方向就应该从 0 指向 1 ,key = 0,value 包含 1, 如下图所示:

现在建图这一步我们已经完成,根据上面的拓扑排序实现具体步骤我们可以知道,我们需要将图中入度为 0 的顶点加入到队列中。在上面的代码实现中,我们定义数组 in
来记录元素入度的次数,因为题目已知课程名字记为 [0~n-1] ,所以说定义数组刚好通过int[] in=new int[n];就可以实现,下标表示课程名字。数组初始值默认为0,上述代码中 b 表示 a 执行的条件,所以说箭头应是 b->a,就应该是 in[a]++ 。
代码解析part2
java
for(int i=0;i<n;i++){
if(in[i]==0) queue.offer(i);
}
寻找出入度为 0 的起始顶点,并将其加入到队列中
代码解析part3
java
while(!queue.isEmpty()){
int size=queue.size();
while(size-->0){
int cur=queue.poll();
List<Integer> list=map.getOrDefault(cur,new ArrayList<>());
for(int x:list){
in[x]--;
if(in[x]==0) queue.offer(x);
}
}
}
queue.size()记录当前入度为 0 的顶点的个数,queue.poll()弹出顶点,map.getOrDefault(cur,new ArrayList<>());查找当前顶点是否是其他顶点的前提条件,如若有返回对应的 list 集合,若没有返回空集合,for(int x:list)遍历返回的 list 数组,in[x]--当前订单所指向的所有顶点入度数 -1 , if(in[x]==0) queue.offer(x)随后进行判断,是否入度数为 0 ,若为 0 加入队列。此步骤是在实现拓扑排序步骤中的第二步。
代码解析part4
java
for(int i=0;i<n;i++){
if(in[i]!=0) return false;
}
if(in[i]!=0)循环判定,当前所有顶点的入度是否均为0,如为0表示所有课程均可学完,若存在顶点值不为0,表示当前课程未能进行学习,返回false,否则返回true。
加餐练习题
加餐题和上文题思想不能说毫不相干,简直一模一样!
点击这里进行跳转 --> 课程表II
本篇文章看到这里已经渐渐步入尾声了,希望各位小伙伴看完本文后对拓扑排序会有一个新的认识,也要记得吃掉加餐嗷。以上就是本期分享的全部内容啦~~上文有不对的地方欢迎大家在评论区进行指正。


