【Leetcode hot 100】207.课程表
问题链接
207.课程表
问题描述
你这个学期必须选修 numCourses
门课程,记为 0
到 numCourses - 1
。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites
给出,其中 prerequisites[i] = [ai, bi]
,表示如果要学习课程 ai
则 必须 先学习课程 bi
。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
提示:
1 <= numCourses <= 2000
0 <= prerequisites.length <= 5000
prerequisites[i].length == 2
0 <= ai, bi < numCourses
prerequisites[i]
中的所有课程对 互不相同
问题解答
问题核心是判断有向图是否存在环(课程依赖关系构成有向图,环代表循环依赖,无法完成所有课程)。
问题分析
- 模型抽象:将课程视为「节点」,先修关系
[ai, bi]
视为「从 bi 指向 ai 的有向边」(学 ai 必须先学 bi)。 - 核心目标:判断该有向图是否为「有向无环图(DAG)」,若是则返回
true
,否则返回false
。
解法 1:Kahn 算法(BFS 拓扑排序)
思路
Kahn 算法是基于「入度」的拓扑排序方法,步骤如下:
- 构建图结构:用邻接表
adj
存储节点间的依赖关系,用inDegree
数组存储每个节点的入度(即前置依赖课程数)。 - 初始化队列:将所有入度为 0 的节点(无前置依赖的课程)加入队列。
- 拓扑遍历:依次取出队列中的节点,减少其所有邻接节点的入度;若邻接节点入度变为 0,加入队列。
- 判断结果:统计遍历的节点总数,若等于课程数
numCourses
,说明无环(可完成所有课程),否则有环。
Java 代码
import java.util.LinkedList;
import java.util.Queue;
import java.util.ArrayList;
import java.util.List;class Solution {// Kahn 算法(BFS 拓扑排序)public boolean canFinish(int numCourses, int[][] prerequisites) {// 1. 初始化邻接表(存储图)和入度数组List<List<Integer>> adj = new ArrayList<>(); // adj[u] 表示 u 的所有后继节点(学完 u 可学的课程)int[] inDegree = new int[numCourses]; // inDegree[v] 表示 v 的入度(学 v 需先学的课程数)// 给每个课程初始化邻接表项for (int i = 0; i < numCourses; i++) {adj.add(new ArrayList<>());}// 2. 填充邻接表和入度数组for (int[] pre : prerequisites) {int ai = pre[0]; // 目标课程(需先学 bi)int bi = pre[1]; // 先修课程adj.get(bi).add(ai); // 构建 bi -> ai 的边inDegree[ai]++; // ai 的入度 +1(多了一个前置依赖)}// 3. 初始化队列:入度为 0 的节点(无前置依赖的课程)Queue<Integer> queue = new LinkedList<>();for (int i = 0; i < numCourses; i++) {if (inDegree[i] == 0) {queue.offer(i);}}// 4. 拓扑遍历,统计可完成的课程数int count = 0;while (!queue.isEmpty()) {int curr = queue.poll(); // 取出当前可学的课程count++; // 完成课程数 +1// 遍历当前课程的所有后继课程,减少其入度for (int next : adj.get(curr)) {inDegree[next]--;// 若后继课程入度变为 0,加入队列(可学了)if (inDegree[next] == 0) {queue.offer(next);}}}// 5. 若完成的课程数等于总课程数,说明无环;否则有环return count == numCourses;}
}
解法 2:DFS 环检测
思路
DFS 基于「递归栈」检测环:通过状态数组标记节点的访问状态,判断递归过程中是否回到「正在访问的节点」(即环)。
- 状态定义:
0
:未访问过;1
:正在访问(处于当前递归栈中,未回溯完成);2
:已访问(递归完成,无环)。
- 递归逻辑:
- 遍历每个未访问节点,启动 DFS;
- 若当前节点状态为
1
,说明遇到环,返回false
; - 标记当前节点为
1
(进入递归栈),递归访问所有邻接节点; - 递归完成后,标记当前节点为
2
(退出递归栈);
- 结果判断:若所有节点遍历完成未检测到环,返回
true
。
Java 代码
import java.util.ArrayList;
import java.util.List;class Solution {// DFS 环检测public boolean canFinish(int numCourses, int[][] prerequisites) {// 1. 构建邻接表(存储图)List<List<Integer>> adj = new ArrayList<>();for (int i = 0; i < numCourses; i++) {adj.add(new ArrayList<>());}for (int[] pre : prerequisites) {int ai = pre[0];int bi = pre[1];adj.get(bi).add(ai); // bi -> ai 的边(学 ai 需先学 bi)}// 2. 状态数组:0=未访问,1=正在访问(递归栈中),2=已访问int[] status = new int[numCourses];// 3. 遍历所有节点,检测环for (int i = 0; i < numCourses; i++) {if (status[i] == 0) { // 未访问的节点才需要 DFSif (hasCycle(i, adj, status)) {return false; // 检测到环,直接返回 false}}}// 4. 无环,返回 truereturn true;}// 递归函数:检测从 curr 节点出发是否有环private boolean hasCycle(int curr, List<List<Integer>> adj, int[] status) {// 若当前节点正在访问(递归栈中),说明有环if (status[curr] == 1) {return true;}// 若当前节点已访问,无环if (status[curr] == 2) {return false;}// 标记当前节点为「正在访问」status[curr] = 1;// 递归访问所有邻接节点(后继课程)for (int next : adj.get(curr)) {if (hasCycle(next, adj, status)) {return true; // 子节点有环,当前也有环}}// 递归完成,标记当前节点为「已访问」status[curr] = 2;// 无环return false;}
}
复杂度分析
两种解法的时间/空间复杂度一致:
- 时间复杂度:
O(V + E)
,其中V = numCourses
(节点数),E = prerequisites.length
(边数)。需遍历所有节点和边。 - 空间复杂度:
O(V + E)
,邻接表存储边需O(E)
,队列/递归栈/状态数组存储节点需O(V)
。
示例验证
- 示例 1:
numCourses=2, prerequisites=[[1,0]]
邻接表:adj[0] = [1], adj[1] = []
,入度[0,1]
。Kahn 算法队列先入 0,取出后 1 入度变为 0,最终 count=2,返回true
。 - 示例 2:
numCourses=2, prerequisites=[[1,0],[0,1]]
邻接表:adj[0] = [1], adj[1] = [0]
,入度[1,1]
。Kahn 队列无初始节点,count=0≠2,返回false
;DFS 会检测到状态 1 的节点,返回false
。