题目:
现在总共有 numCourses 门课需要选,记为 0 到 numCourses-1。
给定一个数组 prerequisites ,它的每一个元素 prerequisites[i] 表示两门课程之间的先修顺序。
例如 prerequisites[i] = [ai, bi] 表示想要学习课程 ai ,需要先完成课程 bi 。
请根据给出的总课程数 numCourses 和表示先修顺序的 prerequisites 得出一个可行的修课序列。
可能会有多个正确的顺序,只要任意返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
--------------------
示例 1:
输入: numCourses = 2, prerequisites = [[1,0]]
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:
输入: numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。
并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
示例 3:
输入: numCourses = 1, prerequisites = []
输出: [0]
解释: 总共 1 门课,直接修第一门课就可。
提示:
1 <= numCourses <= 2000
0 <= prerequisites.length <= numCourses * (numCourses - 1)
prerequisites[i].length == 2
0 <= ai, bi < numCourses
ai != bi
prerequisites 中不存在重复元素
-----------------
思路:
先说最重要的部分:「拓扑排序」是专门应用于有向图的算法;
这道题用 BFS 和 DFS 都可以完成,只需要掌握 BFS 的写法就可以了,BFS 的写法很经典;
BFS 的写法就叫「拓扑排序」,这里还用到了贪心算法的思想,
贪的点是:当前让入度为 0 的那些结点入队;
「拓扑排序」的结果不唯一;
删除结点的操作,通过「入度数组」体现,这个技巧要掌握;
「拓扑排序」的一个附加效果是:能够顺带检测有向图中是否存在环,这个知识点非常重要,
如果在面试的过程中遇到这个问题,要把这一点说出来。
具有类似附加功能的算法还有:Bellman-Ford 算法附加的作用是可以用于检测是否有负权环
--------------------
如果优先图中,存在环,拓扑排序不能继续得到入度值为 0 的节点,退出循环,
此时图中存在没有遍历到的节点,说明图中存在环。
此时说明课程设计不合理,有循环依赖。
拓扑排序实际上应用的是贪心算法,贪心算法简而言之:每一步最优,则全局最优。
具体到拓扑排序,每一次都从图中删除没有前驱的顶点,这里并不需要真正的做删除操作,
我们可以设置一个入度数组,每一轮都输出入度为 0 的结点,并移除它、修改它指向的结点的入度(-1即可),
依次得到的结点序列就是拓扑排序的结点序列。如果图中还有结点没有被移除,
则说明“不能完成所有课程的学习”。
拓扑排序保证了每个活动(在这题中是“课程”)的所有前驱活动都排在该活动的前面,
并且可以完成所有活动。拓扑排序的结果不唯一。拓扑排序还可以用于检测一个有向图是否有环。
相关的概念还有 AOV 网,这里就不展开了。
算法流程:
1、在开始排序前,扫描对应的存储空间(使用邻接表),将入度为 0 的结点放入队列。
2、只要队列非空,就从队首取出入度为 0 的结点,将这个结点输出到结果集中,
并且将这个结点的所有邻接结点(它指向的结点)的入度减 1,在减 1 以后,
如果这个被减 1 的结点的入度为 0 ,就继续入队。
3、当队列为空的时候,检查结果集中的顶点个数是否和课程数相等即可。
(思考这里为什么要使用队列?如果不用队列,还可以怎么做,会比用队列的效果差还是更好?)
在代码具体实现的时候,除了保存入度为 0 的队列,我们还需要两个辅助的数据结构:
1、邻接表:通过结点的索引,我们能够得到这个结点的后继结点;
2、入度数组:通过结点的索引,我们能够得到指向这个结点的结点个数。
这个两个数据结构在遍历题目给出的邻边以后就可以很方便地得到。
----------------
```java
复杂度分析:
时间复杂度:O(E + V)。这里 E 表示邻边的条数,V 表示结点的个数。
初始化入度为 0 的集合需要遍历整张图,具体做法是检查每个结点和每条边,
因此复杂度为 O(E+V),然后对该集合进行操作,又需要遍历整张图中的每个结点和每条边,
复杂度也为 O(E+V)
空间复杂度:O(V):入度数组、邻接表的长度都是结点的个数 V,
即使使用队列,队列最长的时候也不会超过 V,因此空间复杂度是 O(V)
--------------------
BFS 的总体思路:
建立入度表,入度为 0 的节点先入队
当队列不为空,节点出队,标记学完课程数量的变量加 1,并记录该课程
将课程的邻居入度减 1
若邻居课程入度为 0,加入队列
用一个变量记录学完的课程数量,一个数组记录最终结果,简洁好理解。
-------------------
class Solution { // 方法 1 最简单的 BFS
public int[] findOrder(int numCourses, int[][] prerequisites) {
if (numCourses == 0) return new int[0];
int[] inDegrees = new int[numCourses];
for (int[] p : prerequisites) { // 建立入度表 对于有先修课的课程,计算有几门先修课
inDegrees[p[0]]++;
}
Queue<Integer> queue = new LinkedList<>();// 入度为0的节点队列
for (int i = 0; i < inDegrees.length; i++) {
if (inDegrees[i] == 0) queue.offer(i);
}
int count = 0; // 记录可以学完的课程数量
int[] res = new int[numCourses]; // 可以学完的课程
while (!queue.isEmpty()){ // 根据提供的先修课列表,删除入度为 0 的节点
int curr = queue.poll();
res[count++] = curr; // 将可以学完的课程加入结果当中
for (int[] p : prerequisites) {
if (p[1] == curr){
inDegrees[p[0]]--;
if (inDegrees[p[0]] == 0) queue.offer(p[0]);
}
}
}
if (count == numCourses) return res;
return new int[0];
}
}