LeetCode 207中等拓扑排序
课程表 图解题解
这道题到底在问什么
有些课要先修另一门才能上。给定先修关系,判断能否修完所有课。
- 课程数
- 4 门(0,1,2,3)
- 先修
- 学2先学0和1;学3先学2
- 输出
- true
最优解:一步一步想明白
- 3最朴素的做法:每轮扫一遍所有课,找还没学、且前置都学完的去学。可每轮都要把每门课的前置重新检查一次,n 门课最坏扫 n 轮——大量重复。
- 4转折:与其每轮重数,不如给每门课维护一个入度(指向它的箭头数 = 还有几门前置没学)。学完一门课,只把它直接解锁的课入度各减 1;谁减到 0,就立刻能学、入队。这样每条边只用一次,不再重复扫。
- 5indeg = [0,0,2,1]邻接表
graph[0]=[2]、graph[1]=[2]、graph[2]=[3],并数出入度:课 0、1 没前置(0),课 2 等两门(2),课 3 等一门(1)。 - 6q = [0, 1]把所有入度为 0 的课入队:0 和 1 都没前置,先放进队列,作为「现在就能学」的起点。
- 7cnt = 1取出 0 学完,已学 cnt=1。接下来看它解锁了哪些课(graph[0]=[2])。
- 8indeg[2]: 2 → 1沿边 0→2,把课 2 入度 −1:2 还没到 0(还差课 1)——所以课 2 暂不入队。这一步很关键:减入度 ≠ 立即入队,只有归零才入队。
- 9cnt = 2再取出 1 学完,cnt=2。它也解锁课 2(graph[1]=[2])。
- 10indeg[2]: 1 → 0 ✓沿边 1→2,课 2 入度 1→0:两门前置都学完了!课 2 现在入队。对比上一步——同样是减入度,这次归零所以入队。
- 11cnt = 3,q = [3]取出 2 学完,cnt=3。沿 2→3,课 3 入度 1→0,入队。
- 12cnt = 4 = n → true取出 3,cnt=4 = 课程总数 n。4 门全部成功出队 → 没有环,返回 true。若图里有环,环上的课入度永远减不到 0、进不了队,最终 cnt < n,就返回 false。
- 15凡是「有依赖顺序、判断能否排出顺序/有无环」,都是拓扑排序:课程表 II、任务调度都靠它。
- 20把这题练到能复述后,再去同类题里迁移:先复用状态定义,再调整边界条件。
⚠️ 容易写错的地方
✗ 错:建图方向搞反
✓ 对:[a,b] 表示先学 b → 边 b→a
方向反了入度全错
✗ 错:只判断队列空就返回 true
✓ 对:要数出队个数 cnt == n
有环时环上节点出不了队,cnt < n
完整代码(Python / C++ / Java)
Python
class Solution:
def canFinish(self, numCourses, prerequisites):
graph = [[] for _ in range(numCourses)]
indeg = [0] * numCourses
for a, b in prerequisites:
graph[b].append(a)
indeg[a] += 1
q = [i for i in range(numCourses) if indeg[i] == 0]
head = 0
seen = 0
while head < len(q):
cur = q[head]
head += 1
seen += 1
for nxt in graph[cur]:
indeg[nxt] -= 1
if indeg[nxt] == 0:
q.append(nxt)
return seen == numCoursesC++
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> graph(numCourses);
vector<int> indeg(numCourses, 0);
for (auto& p : prerequisites) { graph[p[1]].push_back(p[0]); indeg[p[0]]++; }
queue<int> q;
for (int i = 0; i < numCourses; i++) if (indeg[i] == 0) q.push(i);
int seen = 0;
while (!q.empty()) {
int cur = q.front(); q.pop(); seen++;
for (int nxt : graph[cur]) if (--indeg[nxt] == 0) q.push(nxt);
}
return seen == numCourses;
}
};Java
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < numCourses; i++) graph.add(new ArrayList<>());
int[] indeg = new int[numCourses];
for (int[] p : prerequisites) { graph.get(p[1]).add(p[0]); indeg[p[0]]++; }
Queue<Integer> q = new LinkedList<>();
for (int i = 0; i < numCourses; i++) if (indeg[i] == 0) q.offer(i);
int seen = 0;
while (!q.isEmpty()) {
int cur = q.poll(); seen++;
for (int nxt : graph.get(cur)) if (--indeg[nxt] == 0) q.offer(nxt);
}
return seen == numCourses;
}
}套路模板 · 拓扑排序「入度0入队 + 数出队」(背下来)
# 「有依赖顺序,判能否排序 / 有无环」都套
indeg = [0]*n; g = defaultdict(list)
for u, v in 依赖: g[u].append(v); indeg[v] += 1 # u→v
q = deque(i for i in range(n) if indeg[i]==0)
cnt = 0
while q:
x = q.popleft(); cnt += 1
for y in g[x]:
indeg[y] -= 1
if indeg[y]==0: q.append(y)
return cnt == n # 全出队才无环class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> graph(numCourses);
vector<int> indeg(numCourses, 0);
for (auto& p : prerequisites) { graph[p[1]].push_back(p[0]); indeg[p[0]]++; }
queue<int> q;
for (int i = 0; i < numCourses; i++) if (indeg[i] == 0) q.push(i);
int seen = 0;
while (!q.empty()) {
int cur = q.front(); q.pop(); seen++;
for (int nxt : graph[cur]) if (--indeg[nxt] == 0) q.push(nxt);
}
return seen == numCourses;
}
};class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < numCourses; i++) graph.add(new ArrayList<>());
int[] indeg = new int[numCourses];
for (int[] p : prerequisites) { graph.get(p[1]).add(p[0]); indeg[p[0]]++; }
Queue<Integer> q = new LinkedList<>();
for (int i = 0; i < numCourses; i++) if (indeg[i] == 0) q.offer(i);
int seen = 0;
while (!q.isEmpty()) {
int cur = q.poll(); seen++;
for (int nxt : graph.get(cur)) if (--indeg[nxt] == 0) q.offer(nxt);
}
return seen == numCourses;
}
}复杂度
时间复杂度
O(V+E)
每个节点和边各处理一次
空间复杂度
O(V+E)
邻接表 + 入度 + 队列
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 课程表 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
拓扑排序的两种实现?+
① BFS(Kahn)反复取入度 0 的点 ② DFS 后序 + 判后向边(环)。都 O(V+E)。
怎么判断有环?+
BFS 若最终修完课数 < 总课数,剩下的卡在环里;DFS 遇到正在访问的节点=后向边=环。
复杂度?+
O(V+E):建图 + 每点每边一次。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。