一、核心思路
本题等价于判断有向图是否存在环
- 课程对应图的节点;
prerequisites[i] = [a,b]:学习 a 必须先学 b → 构建有向边b → a - Kahn(BFS 拓扑排序)思想:
- 构建邻接表存储图,用数组记录每个节点的入度(该课程还有几门前置课未完成)
- 将入度为 0 的节点(无前置课程)加入队列
- 不断从队列取出节点,计数 + 1;它的后继节点入度 - 1,后继入度变为 0 则入队
- 最后比较:成功遍历的节点数 count 是否等于总课程数。相等说明无环,可以修完所有课;count 更小说明存在环,无法完成。
二、完整 Java 代码
import java.util.*;
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
//邻接表,存每个节点的后继节点
List<List<Integer>> adj = new ArrayList<>();
//inDegree:记录每门课的入度(前置课程数量)
int[] inDegree = new int[numCourses];
//初始化邻接表,给每一个课程分配一个空列表
for(int i = 0; i < numCourses; i++){
adj.add(new ArrayList<>());
}
//建图 + 更新入度
for(int[] p : prerequisites){
int a = p[0];
int b = p[1];
adj.get(b).add(a); //边 b -> a,b学完才可以学a
inDegree[a]++;
}
Queue<Integer> q = new LinkedList<>();
//入度为0的课程入队(可以直接学习)
for(int i = 0; i < numCourses; i++){
if(inDegree[i] == 0){
q.offer(i);
}
}
int count = 0; //统计已经学完的课程
while(!q.isEmpty()){
int cur = q.poll();
count++;
//遍历cur所有后继课程
for(int next : adj.get(cur)){
inDegree[next]--;
if(inDegree[next] == 0){
q.offer(next);
}
}
}
//所有课程都遍历到→无环;否则有环
return count == numCourses;
}
}
三、实例表格演示
示例 1:numCourses = 2,prerequisites = [[1,0]]
边:0 → 1
| 节点 | 初始入度 |
|---|---|
| 0 | 0 |
| 1 | 1 |
| 步骤 | 队列 | count | 操作 |
|---|---|---|---|
| 1 | [0] | 0 | 初始化,0 入度为 0 入队 |
| 2 | [ ] | 1 | 弹出 0,count=1;后继 1 入度减为 0,1 入队 |
| 3 | [1] | 1 | 队列现在是 [1] |
| 4 | [ ] | 2 | 弹出 1,count=2;无后继 |
判断:2 == 2 → 返回 true ✅
示例 2:numCourses = 2,prerequisites = [[1,0],[0,1]]
边:0 → 1,1 → 0
| 节点 | 初始入度 |
|---|---|
| 0 | 1 |
| 1 | 1 |
| 步骤 | 队列 | count | 操作 |
|---|---|---|---|
| 1 | [ ] | 0 | 没有入度 = 0 的节点,队列为空,循环不执行 |
判断:0 != 2 → 返回 false ❌
四、复杂度分析
- 时间复杂度:(O(n+m)),n 是课程总数,m 是前置条件数量,每个节点、每条边只会访问一次
- 空间复杂度:(O(n+m)),邻接表、入度数组、队列占用空间
五、易错点总结
- 边方向极易写反:
[a,b]代表学 a 要先学 b,是b→a,写反整个拓扑排序失效 - 必须先初始化邻接表,给每个课程分配空列表,否则
adj.get()下标越界 - 环内节点入度永远无法变成 0,不会被 count 统计,count 一定小于总课程数
六、个人感悟
拓扑排序专门用来解决有向图判环、依赖顺序安排类题目,Kahn BFS 模板固定。核心记住:入度代表还剩多少依赖,依赖全部消除(入度 = 0)才可以开始学习该节点。