整体思路
1. 核心问题
题目要求判断是否所有课程都能完成。课程之间的先修关系构成了有向图。
- 如果图中存在环(例如 A 是 B 的先修,B 又是 A 的先修),则产生死锁,无法完成所有课程。
- 因此,问题转化为:检测有向图中是否存在环。
2. 算法逻辑
代码使用了 DFS 进行遍历,并利用 flag 数组记录每个节点的状态(典型的三色标记法):
- 0 (未访问):节点尚未被访问。
- 1 (正在访问) :节点在当前的递归栈中。如果在 DFS 过程中遇到了状态为
1的节点,说明绕了一圈又回到了当前路径上的节点,即发现了环。 - -1 (已访问/安全) :节点及其所有后代节点都已经检查过,且没有发现环。如果遇到状态为
-1的节点,无需再次遍历,直接剪枝返回安全。
3. 具体步骤
- 建图 :使用邻接表
g存储图结构。根据输入prerequisites,如果[0, 1]表示修课程 0 必须先修 1,则建立一条边1 -> 0。 - 全图遍历 :因为图可能是不连通的(包含多个独立的连通分量),所以需要对外层所有节点
0到numCourses-1进行循环,确保每个节点都被检查到。 - DFS 递归 :
- 进入节点
i,标记为1(正在访问)。 - 遍历
i的所有邻居(后续课程)。 - 如果邻居返回
false(发现环),则层层向上返回false。 - 所有邻居遍历完且无环,将节点
i标记为-1(安全),并返回true。
- 进入节点
完整代码
java
import java.util.ArrayList;
import java.util.List;
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 1. 构建邻接表 (Adjacency List) 来表示有向图
// 索引 i 代表先修课程,g.get(i) 存储依赖于 i 的后续课程列表
List<List<Integer>> g = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
g.add(new ArrayList<>());
}
// 填充邻接表
// input: [0, 1] 表示修课程 0 需先修 1,即边为 1 -> 0
for (int[] e : prerequisites) {
g.get(e[1]).add(e[0]);
}
// flag 数组用于记录每个课程节点的访问状态 (三色标记法)
// 0: 未访问 (Unvisited)
// 1: 正在访问 (Visiting, 当前递归栈中)
// -1: 已访问且安全 (Visited, 确定无环)
int[] flag = new int[numCourses];
// 2. 遍历所有课程,处理图可能不连通的情况
for (int i = 0; i < numCourses; i++) {
// 对每个未完全确认安全的节点进行 DFS
// 如果 dfs 返回 false,说明检测到了环,直接返回 false
if (!dfs(g, flag, i)) {
return false;
}
}
// 如果所有节点都检查完毕且没有发现环,返回 true
return true;
}
// 深度优先搜索辅助函数
// 返回 true 表示从节点 i 出发没有环,返回 false 表示有环
private boolean dfs(List<List<Integer>> g, int[] flag, int i) {
// 情况 1: 当前节点标记为 1,说明在当前递归路径中再次遇到了它 -> 发现环
if (flag[i] == 1) return false;
// 情况 2: 当前节点标记为 -1,说明之前已经检查过该节点及其后续路径,是安全的 -> 剪枝
if (flag[i] == -1) return true;
// 情况 3: 当前节点为 0 (未访问),开始访问
// 将状态标记为 1 (正在访问)
flag[i] = 1;
// 递归访问当前课程的所有后续课程 (邻居)
for (Integer e : g.get(i)) {
// 如果后续路径中发现环,直接向上返回 false
if (!dfs(g, flag, e)) {
return false;
}
}
// 所有邻居都检查完毕,没有发现环
// 将状态标记为 -1 (已访问且安全),避免重复计算
flag[i] = -1;
return true;
}
}
时空复杂度
假设课程数量(节点数)为 VVV(即 numCourses),先修关系数量(边数)为 EEE(即 prerequisites.length)。
1. 时间复杂度:O(V+E)O(V + E)O(V+E)
- 建图 :遍历所有边,耗时 O(E)O(E)O(E);初始化邻接表耗时 O(V)O(V)O(V)。
- DFS 遍历 :
- 由于使用了
flag数组进行记忆化(剪枝),每个节点最多被访问一次(从状态 0 变为 1,再变为 -1)。 - 在 DFS 过程中,每条边也最多被检查一次。
- 因此 DFS 的总时间复杂度是 O(V+E)O(V + E)O(V+E)。
- 由于使用了
- 总计 :O(V+E)O(V + E)O(V+E)。
2. 空间复杂度:O(V+E)O(V + E)O(V+E)
- 邻接表
g:需要存储 VVV 个列表头和 EEE 条边,空间为 O(V+E)O(V + E)O(V+E)。 - 标记数组
flag:长度为 VVV,空间为 O(V)O(V)O(V)。 - 递归栈 :在最坏情况下(例如图退化为一条长链),递归深度可达 VVV,空间为 O(V)O(V)O(V)。
- 总计 :O(V+E)O(V + E)O(V+E)。