一、题目描述
现在你总共有 numCourses
门课需要选,记为 0
到 numCourses - 1
。给你一个数组 prerequisites
,其中 prerequisites[i] = [ai, bi]
,表示在选修课程 ai
前 必须 先选修 bi
。
- 例如,想要学习课程
0
,你需要先完成课程1
,我们用一个匹配来表示:[0,1]
。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。
示例 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,2,1,3] 解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。 因此,一个正确的课程顺序是[0,1,2,3]
。另一个正确的排序是[0,2,1,3]
。
示例 3:
输入:numCourses = 1, prerequisites = [] 输出:[0]
提示:
1 <= numCourses <= 2000
0 <= prerequisites.length <= numCourses * (numCourses - 1)
prerequisites[i].length == 2
0 <= ai, bi < numCourses
ai != bi
- 所有
[ai, bi]
互不相同
二、解题思路
这个问题可以通过拓扑排序来解决。拓扑排序是针对有向无环图(DAG)的一种排序算法,它会返回一个顶点的线性序列,这个序列满足图中所有的有向边都从序列的前面指向后面。
以下是解题思路:
- 创建一个入度数组,用于记录每个顶点的入度(即有多少边指向它)。
- 创建一个队列,用于存储所有入度为0的顶点(这些顶点没有先修课程,可以立即学习)。
- 当队列非空时,从队列中弹出一个顶点,将该顶点添加到结果列表中,并减少其所有邻接顶点的入度。如果某个邻接顶点的入度变为0,则将其加入队列。
- 如果结果列表中的顶点数量等于课程数,则返回结果列表;否则,说明存在环,返回空数组。
三、具体代码
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class Solution {
public int[] findOrder(int numCourses, int[][] prerequisites) {
// 1. 创建入度数组
int[] inDegree = new int[numCourses];
// 2. 创建邻接表
List<List<Integer>> adjList = new ArrayList<>(numCourses);
for (int i = 0; i < numCourses; i++) {
adjList.add(new ArrayList<>());
}
// 3. 填充入度数组和邻接表
for (int[] prerequisite : prerequisites) {
inDegree[prerequisite[0]]++;
adjList.get(prerequisite[1]).add(prerequisite[0]);
}
// 4. 创建队列,存储所有入度为0的顶点
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) {
queue.offer(i);
}
}
// 5. 拓扑排序
int[] order = new int[numCourses];
int index = 0;
while (!queue.isEmpty()) {
int current = queue.poll();
order[index++] = current;
for (int next : adjList.get(current)) {
inDegree[next]--;
if (inDegree[next] == 0) {
queue.offer(next);
}
}
}
// 6. 检查是否所有课程都被安排了
if (index == numCourses) {
return order;
} else {
return new int[0]; // 如果存在环,返回空数组
}
}
}
四、时间复杂度和空间复杂度
1. 时间复杂度
- 初始化入度数组和邻接表:我们需要遍历所有课程(
numCourses
),并初始化每个课程的入度数组和邻接表。这一步的时间复杂度是 O(numCourses)。 - 填充入度数组和邻接表:我们需要遍历所有的先修课程(
prerequisites
),对于每个先修课程对,我们需要更新入度数组和邻接表。这一步的时间复杂度是 O(prerequisites.length)。 - 将所有入度为0的课程加入队列:我们需要再次遍历所有课程,检查每个课程的入度。这一步的时间复杂度是 O(numCourses)。
- 进行拓扑排序:在拓扑排序过程中,每个课程最多只会被添加到队列一次,并且从队列中移除一次。因此,这一步的时间复杂度是 O(numCourses + prerequisites.length),因为每个先修课程对都会被访问一次。
综上所述,总的时间复杂度是 O(numCourses + prerequisites.length)。
2. 空间复杂度
- 入度数组:我们创建了一个大小为
numCourses
的数组来存储每个课程的入度。因此,空间复杂度是 O(numCourses)。 - 邻接表:邻接表中的每个课程最多会有
numCourses - 1
个邻接课程(在最坏的情况下,每个课程都依赖于其他所有课程)。因此,邻接表的空间复杂度是 O(numCourses * (numCourses - 1))。 - 队列:在最坏的情况下,队列可能需要存储所有课程,因此空间复杂度是 O(numCourses)。
- 结果数组:结果数组的大小是
numCourses
,因此空间复杂度是 O(numCourses)。
将上述空间复杂度相加,我们得到总的空间复杂度是 O(numCourses + numCourses * (numCourses - 1)),即 O(numCourses^2)。在最坏的情况下,这个表达式可以简化为 O(numCourses^2),因为当 numCourses
较大时,numCourses^2
将是主导项。然而,在实际情况下,由于每个课程不可能都依赖于其他所有课程,因此实际的空间复杂度通常会小于这个上限。
五、总结知识点
-
数组:代码中使用了
int[]
类型的数组来存储每个课程的入度,即inDegree
数组。 -
列表:使用
List<List<Integer>>
类型的列表来创建邻接表,表示每个课程指向的其他课程。 -
队列:使用
Queue<Integer>
类型的队列来存储所有入度为0的课程,帮助实现拓扑排序。 -
循环:代码中使用了
for
循环来初始化入度数组、邻接表,以及填充这些数据结构。 -
条件语句:使用了
if
语句来检查入度是否为0,并据此将课程加入队列。 -
链表:
LinkedList
类被用作队列的实现,它提供了offer
和poll
方法来添加和移除队列元素。 -
拓扑排序:代码的核心是拓扑排序算法,这是一种针对有向无环图(DAG)的排序算法。
-
图论:代码解决的是图论中的问题,特别是有向图的拓扑排序问题。
-
异常处理:虽然代码中没有显式的异常处理,但返回空数组
new int[0]
是处理无法完成所有课程(即存在环)的情况。 -
数据结构:代码中使用了多种数据结构,包括数组、列表、队列,来存储和处理图的数据。
-
算法设计:代码展示了如何设计算法来解决问题,包括如何处理输入数据,执行计算,并返回结果。
-
递归的替代:虽然拓扑排序通常可以用递归来实现,但此代码示例使用迭代(循环)方法,避免了递归可能带来的栈溢出问题。
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。