LeetCode 210中等拓扑排序
课程表 II 图解题解
这道题到底在问什么
给定先修关系,返回一个能修完所有课的学习顺序;若有环修不完,返回空数组。
- 课程数
- 4 门(0,1,2,3)
- 先修
- 学 1、2 前先学 0;学 3 前先学 1 和 2
- 输出
- [0, 1, 2, 3]
最优解:一步一步想明白
- 3最朴素的做法:每轮扫一遍所有课,挑还没学、且前置都学完的去学。可每轮都要把每门课的前置重新检查一次,n 门课最坏扫 n 轮——大量重复。
- 4转折:与其每轮重数,不如给每门课维护入度(指向它的箭头数 = 还差几门前置)。入度 0 的先入队;每次出队一门课就追加进 order,再把它解锁的课入度各减 1,谁减到 0 才入队。这样每条边只用一次,order 收集的顺序就是拓扑序。
- 5indeg = [0,1,1,2]邻接表
g[0]=[1,2]、g[1]=[3]、g[2]=[3],数出入度:课 0 没前置(0),课 1、2 各等一门(1),课 3 等两门(2)。order 现在还空。 - 6q = [0]把入度为 0 的课入队:只有课 0 没前置,先放进队列,作为「现在就能学」的起点。
- 7order = [0]取出 0 学完,第一时间把它追加进 order=[0]——出队顺序就是答案顺序。接着看它解锁了哪些课:g[0]=[1,2]。
- 8indeg[1]: 1 → 0 ✓沿边 0→1,课 1 入度 1→0:它唯一的前置 0 学完了,立刻入队。
- 9indeg[2]: 1 → 0 ✓再沿边 0→2,课 2 入度也减到 0,入队。现在队列 [1,2] 都待学,0 处理完变绿。 队列里 1、2 谁先弹取决于入队次序,所以 [0,2,1,3] 等也是合法拓扑序——顺序和我不同不代表错。
- 10order = [0,1]取出 1 学完,追加进 order=[0,1]。它解锁课 3:g[1]=[3]。
- 11indeg[3]: 2 → 1沿边 1→3,课 3 入度 2→1:还没到 0(还差课 2)——所以课 3 暂不入队。关键:减入度 ≠ 立即入队,只有归零才入队。
- 12order = [0,1,2]取出 2 学完,追加进 order=[0,1,2]。它也解锁课 3:g[2]=[3]。
- 13indeg[3]: 1 → 0 ✓沿边 2→3,课 3 入度 1→0:两门前置都学完了,入队。对比上一步——同样是减入度,这次归零所以入队。
- 14order = [0,1,2,3],len==n取出 3 学完,order=[0,1,2,3]。长度 4 = 课程数 n,返回这个顺序 [0,1,2,3]。若图有环,环上的课入度永远减不到 0、进不了队,order 长度小于 n,就返回空数组。 反过来:若图里有环,环上的课入度永远减不到 0、进不了队,order 长度 < n,就返回 [] 表示无法完成。
- 17凡是「给一堆依赖关系排出可行执行顺序」都是拓扑排序:课程安排、任务调度、编译依赖、Excel 公式求值。
⚠️ 容易写错的地方
✗ 错:不判长度直接返回 order
✓ 对:len(order) != n 时返回空数组
有环时环上节点入度减不到 0、进不了 order,order 不全
✗ 错:减度后无脑入队
✓ 对:只有 indeg[nxt]==0 才 append
前置没学完就入队,顺序会错、还会重复入队
完整代码(Python / C++ / Java)
Python
class Solution:
def findOrder(self, numCourses, prerequisites):
graph = [[] for _ in range(numCourses)]
indeg = [0] * numCourses
for a, b in prerequisites: # 学 a 前要先学 b
graph[b].append(a)
indeg[a] += 1
q = [i for i in range(numCourses) if indeg[i] == 0] # 入度0先学
order = []; head = 0
while head < len(q):
cur = q[head]; head += 1
order.append(cur)
for nxt in graph[cur]:
indeg[nxt] -= 1
if indeg[nxt] == 0: q.append(nxt)
return order if len(order) == numCourses else [] # 有环则空C++
class Solution {
public:
vector<int> findOrder(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);
vector<int> order;
while (!q.empty()) {
int cur = q.front(); q.pop();
order.push_back(cur);
for (int nxt : graph[cur]) if (--indeg[nxt] == 0) q.push(nxt);
}
return order.size() == numCourses ? order : vector<int>{}; // 有环则空
}
};Java
class Solution {
public int[] findOrder(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[] order = new int[numCourses]; int idx = 0;
while (!q.isEmpty()) {
int cur = q.poll(); order[idx++] = cur;
for (int nxt : graph.get(cur)) if (--indeg[nxt] == 0) q.offer(nxt);
}
return idx == numCourses ? order : new int[0]; // 有环则空
}
}套路模板 · 拓扑排序输出顺序(背下来)
# 「有依赖、排一个可行顺序」都套
q = deque(入度0的点); order = []
while q:
u = q.popleft(); order.append(u) # 收集顺序
for v in g[u]:
indeg[v] -= 1
if indeg[v]==0: q.append(v)
return order if len(order)==n else [] # 不全=有环复杂度
时间复杂度
O(V+E)
每门课出队一次、每条先修关系减度一次
空间复杂度
O(V+E)
邻接表 + 入度数组 + 队列 + order
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 课程表 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
核心思路是什么?+
Kahn 拓扑排序:算每门课入度(先修数);入度 0 先入队;每出队一门课就加入答案、把其后继入度减 1,减到 0 再入队。order 长度=课数即有效顺序,否则有环返 []。
和 DFS 拓扑排序有何不同?+
BFS(Kahn) 用入度+队列,直观且天然检测环;DFS 用后序逆序,三色标记检测环。两者都 O(V+E)。
复杂度多少?+
建图 O(E),每点入出队一次、每边松弛一次 → O(V+E);图 + 入度数组 O(V+E) 空间。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。