Hot 100 --- 课程表
本文概览:本文以LeetCode题目"课程表"为例,讲解拓扑排序的BFS入度法——构建邻接表,每次去掉0入度节点,最终用计数器判断是否有环
一、题目

二、题目分析
题目要求:给你 numCourses 门课和 prerequisites 数组,其中 prerequisites[i] = [a, b] 表示要先修 b 才能修 a。判断是否能完成所有课程
本质就是判断图是否有环:如果 A→B→C→A 形成一个环,那这三门课互相依赖,永远没法开始修,返回 false。如果没有环,返回 true
所以这题分两步:
- 构建图:用邻接表(不用邻接矩阵,因为邻接矩阵空间开销太大,利用率不高)
- 判断有没有环:BFS 入度法(拓扑排序)
思路概览
1 | public boolean canFinish(int numCourses, int[][] prerequisites) { |
思路简要说明
- 构建邻接表 + 入度数组:prerequisites[i] = [a, b] 表示 b→a(先修 b 才能修 a),所以 b 的邻接表加入 a,a 的入度+1
- BFS 入度法:先找所有入度为 0 的节点入队(没有前置课,可以直接修)。每出队一个节点,把它指向的节点的入度-1,如果入度变成 0 就入队
- 计数器判断:每出队一个节点 visited+1,最后比较 visited 和 numCourses,相等说明无环,不等说明有环
三、思路详解
第一步:什么是环?
先理解什么叫"有环"。如果课程之间的依赖关系形成了一个圈:
1 | 课程 A 依赖课程 B |
这三门课互相依赖,你没法从任何一门开始修,所以不可能完成。这就是"有环返回 false"的原因
反过来,如果没有环,一定存在至少一门课没有前置课(入度为 0),可以从它开始修
第二步:构建邻接表和入度数组
现在要判断有没有环,但 prerequisites 只是一堆依赖关系,还没构成图的数据结构。我们需要先把它转化成方便操作的图
构建图需要解决两个问题:
- 一个节点修完后,能影响哪些节点? → 需要知道每个节点指向了谁。这就是邻接表的作用——记录每个节点的后续节点
- 一个节点有几个前置课? → 需要知道每个节点被几个节点指向。这就是入度数组的作用——记录每个节点的前置课数量
用 prerequisites 构建图。prerequisites[i] = [a, b] 表示"要先修 b 才能修 a",即 b→a:
1 | 假设 numCourses = 4, prerequisites = [[1,0],[2,1],[3,2]] |
为什么用邻接表不用邻接矩阵?因为邻接矩阵是 numCourses×numCourses 的二维数组,大部分位置是 0(课程之间的依赖关系通常比较稀疏),空间浪费严重。邻接表只存实际有依赖关系的边,空间利用率高
第三步:BFS 入度法——去掉 0 入度节点
有了邻接表和入度数组,怎么判断有没有环?
思考一下:如果图无环,一定有入度为 0 的节点(没有前置课,可以直接修)。把这些节点"修掉"后,剩下的图如果还是无环,又会暴露出新的入度为 0 的节点。一直重复,如果所有节点都能被修掉,说明无环
这里就体现了两个数据结构的配合:
- 入度数组告诉我们当前哪些节点入度为 0,可以修
- 邻接表告诉我们一个节点修完后要去减小谁的入度
用队列实现这个过程:
1 | 初始:从入度数组中找所有入度为 0 的节点,全部入队 |
第四步:完整执行过程图解
以 numCourses = 4, prerequisites = [[1,0],[2,1],[3,2]] 为例:
构建完图后:
1 | 邻接表: 入度数组: |
初始入队:
1 | 入度为 0 的节点:0 |
第 1 轮:
1 | 出队 0,visited = 1 |
第 2 轮:
1 | 出队 1,visited = 2 |
第 3 轮:
1 | 出队 2,visited = 3 |
第 4 轮:
1 | 出队 3,visited = 4 |
最终判断:visited = 4 == numCourses = 4,无环,返回 true
第五步:有环的情况
再看一个有环的例子:numCourses = 3, prerequisites = [[1,0],[2,1],[0,2]]
1 | 含义: |
初始入队:
1 | 入度为 0 的节点:无 |
while 循环不执行,visited = 0
最终判断:visited = 0 ≠ numCourses = 3,有环,返回 false
环的特点就是每个节点都有入度,没有入度为 0 的起点,BFS 根本启动不了
第六步:为什么要用计数器?
有人可能会想:BFS 结束后,直接看入度数组是不是全为 0 不就行了?确实可以,但用计数器更简洁——一个整数 vs 遍历整个数组。而且计数器的含义更直观:visited 记录的是"成功修完的课程数",和 numCourses 一比就知道结果
复杂度分析
- 时间复杂度:O(V + E),V 是课程数,E 是依赖关系数。每个节点入队出队一次,每条边遍历一次
- 空间复杂度:O(V + E),邻接表存所有边,入度数组和队列存所有节点


