文章目录
题目链接:207. 课程表
题目解析
拓扑排序 (Topological sorting )要解决的问题是 如何给一个有向无环图的所有节点排序。
有向无环图 (Directed Acyclic Graph, 缩写 DAG),是一种 边有方向且没有环形结构的图结构。

拓扑排序的起始点是 顶点(入度为0的节点),将顶点排序完毕后,从该顶点发出的有向边全部会被删除,这使得被指向的节点的入度数会减1,当入度数为0时这个节点就成为新了的顶点,然后下一轮从该顶点开始排序,以此重复,直到没有顶点为止。
- 入度:表示指向某节点的有向边数量,
- 出度:表示从该节点发出的有向边数量
构造拓扑序列步骤:
- 从图中选择一个入度为零的点
- 输出该顶点,从图中删除此顶点及其所有的出边
重复上面两步,直到所有顶点都输出,拓扑排序完成,或者图中不存在入度为零的点,此时说明图是有环图,拓扑排序无法完成,陷入死锁。
例子:做一道 青椒炒肉 的过程图如下

按照流程图中的顺序一步一步做出来这道菜的过程,就相当于是 拓扑排序 的过程。
拓扑排序的目标是将所有节点排序,可能出现多个不同的排序结果:
- 买菜 ---> 准备厨具 ---> 洗菜 ---> 腌肉 ---> 切菜 ---> 炒菜 ---> 装盘
- 准备厨具 ---> 买菜 ---> 洗菜 ---> 腌肉 ---> 切菜 ---> 炒菜 ---> 装盘
- 准备厨具 ---> 买菜 ---> 洗菜 ---> 切菜 ---> 腌肉 ---> 炒菜 ---> 装盘
- 准备厨具 ---> 腌肉 ---> 买菜 ---> 洗菜 ---> 切菜 ---> 炒菜 ---> 装盘
如何实现拓扑排序?借助队列,一次多源 BFS 即可:
- 先将所有入度为0的节点放入队列
- 层序遍历:先拿出队首元素并添加到结果中,然后将从该队首元素发出的边全部删去,再判断与这些边相连的节点的入度减一之后的值是否为0(若为0就再加入队列中)
题目给出一个 numCourses 表示本学期要修读的课程数,记作 0 - numCourses-1。
在选修某些课程之前需要一些先修课程。先修课程由数组 prerequisites 给出,其中 prerequisites[i] = [a, b] 表示如果要学习课程 a 则 必须 先学习课程 b。
我们需要判断能否完成所有课程的学习。
例1:numCourses = 2, prerequisites = [[1,0]]
prerequisites[0] = [1, 0] 表示要学习课程 1 必须先学习课程 0,而总课程数 numCourses = 2,这两门课程可能就是课程 1 和课程 0,因此可以先学习课程 0 再学习课程 1 ,是可能完成所有课程的学习的,因此返回 true。
例2:numCourses = 5, prerequisites = [[1,0], [2,0], [3,0], [3,1], [3,2], [4,3]]

学习课程的顺序可以是:0 ---> 1 ---> 2 ---> 3 ---> 4,因此是可以完成所有课程的学习的,返回 true。
算法原理
例:numCourses = 5, prerequisites = [[1,0], [2,0], [3,0], [3,1], [3,2], [4,3]]
根据题目所给,可以画出:

我们需要判断的 "能否完成所有课程的学习" 其实就是判断 有向无环图中是否存在环 ------ 即 能否进行拓扑排序。
因此我们可以用拓扑排序来解决本题。
构造拓扑序列步骤:
- 从图中选择一个入度为零的点
- 输出该顶点,从图中删除此顶点及其所有的出边
重复上面两步,直到所有顶点都输出,拓扑排序完成,或者图中不存在入度为零的点,此时说明图是有环图,拓扑排序无法完成,陷入死锁。
建图
在构造拓扑序列之前,首先要根据题目抽象出图结构。
我们用 邻接表 来表示图的结构,代码实现有两种方式:
- 对于字符串类型的数据,我们通常用 哈希表 (Map < String, List < String >> edges)作为邻接表来映射 "节点,相连的节点列表"。
- 对于整数类型的数据,我们通常用 链表(List< List< Integer >> edges)作为邻接表,用下标(前提是数据从0开始计数)作为 "节点",链表的值作为 "与该节点相连的节点列表"。(也可以使用哈希表来映射:Map < Integer, List < Integer >> edges)
入度数组
本题我们在进行拓扑排序的过程中还需要知道节点的入度值,因此我们为了方便,可以用一个入度数组 in 来记录图中所有节点的入度值。
java
int[] in = new int[numCourses];
代码实现
java
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
// <顶点, 连接的节点列表>
Map<Integer, List<Integer>> edges = new HashMap<>(); // 邻接表存放图
int[] in = new int[numCourses]; // 用于记录每一个顶点的入度值
// 建图
for (int[] prerequisite : prerequisites) {
// prerequisites[a][b] -> 要学习a必须先学习b -> b是a的前提/b有一条路径指向a
int a = prerequisite[0], b = prerequisite[1]; // b -> a
if (!edges.containsKey(b)) { // 判断邻接表中是否存在顶点(b)
edges.put(b, new ArrayList<>()); // 将顶点存入邻接表中
}
edges.get(b).add(a); // 将与顶点连接的节点添加到节点列表
in[a]++; // 更新连接b节点的入度值
}
// 拓扑排序(用来判断图中是否存在环:存在-true/不存在-false)
Queue<Integer> queue = new ArrayDeque<>(); // 队列用于存放入度值为0的顶点
// 1.将入度值为0的顶点放入队列
for (int x = 0; x < numCourses; x++) {
if (in[x] == 0) {
queue.offer(x);
}
}
// 2.层序遍历/BFS
while (!queue.isEmpty()) {
int top = queue.poll(); // 取出队首元素
// 遍历队首元素对应的节点列表,将列表中的所有节点的入度值减一(删除连接线)
for (int x : edges.getOrDefault(top, new ArrayList<>())) {
// 将入度值减一之后判断入度值是否为0(为0时是顶点,要放入队列)
if (--in[x] == 0) {
queue.offer(x);
}
}
}
// 判断图中是否存在环(若入度数组in的所有值都为0则图中不存在环)
for (int x : in) {
if (x != 0) {
// 不为0,有环
return false;
}
}
// 返回true
return true;
}
}
完