# 207. 课程表

力扣题目链接 (opens new window)

# 题目描述

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1。

在选修某些课程之前需要一些先修课程。先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [a, b],表示如果要学习课程 a,必须先完成课程 b。

请判断是否可能完成所有课程。

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:先学习课程 0,再学习课程 1。
1
2
3

示例 2:

输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:两门课程互相依赖,形成了环。
1
2
3

提示:

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 所有课程对互不相同

# 思路

先把课程关系翻译成图。

[a, b] 表示学 a 之前必须学 b,所以应该连一条 b -> a 的有向边。这里千万别把方向写反了。

什么时候所有课程无法完成?课程依赖图中出现环的时候。

例如 0 -> 1 -> 0,想学 0 要先学 1,想学 1 又要先学 0,谁也不能成为第一门课。

拓扑排序不是普通意义上的数值排序,而是把一个有向无环图转换成线性顺序。实现拓扑排序可以使用 Kahn 算法(BFS),也可以使用 DFS,本题掌握更直观的 BFS 即可。

以代码随想录图论章节中的依赖图为例:

为什么肉眼能找到节点 0 作为开头?因为它的入度为 0,没有任何节点指向它。只有这样的节点,才不需要等待其他前置任务。

本题用 Kahn 算法做拓扑排序:

  1. 统计每门课程的入度,入度表示还有多少门先修课没有完成;
  2. 把所有入度为 0 的课程加入队列,它们现在就可以学习;
  3. 每学完一门课,就删除它指向的边,让后续课程的入度减一;
  4. 某门后续课程的入度变为 0 时,加入队列;
  5. 最后比较学完的课程数和 numCourses。

为什么剩下的课程就是环?如果队列已经空了,却还有课程没学,说明剩余每个节点的入度都大于 0。它们互相等待,不可能再找到一个合法起点。

拓扑排序的本质,就是反复找到入度为 0 的节点,将它加入结果集,再将它从图中移除。 代码中不需要真的删除节点,只要把它指向节点的入度减一即可。

# 模拟过程

先找到入度为 0 的节点 0,将它加入结果集:

移除节点 0,也就是将它指向节点的入度减一:

此时节点 1 和节点 2 的入度都变为 0,选哪一个都可以,所以拓扑排序结果可能不唯一。这里先选择节点 1:

移除节点 1 后继续寻找入度为 0 的节点:

不断重复“选入度为 0 的节点、将其指向节点的入度减一”,就能得到一条合法顺序。

如果图中存在有向环,会发生什么?

节点 0 被处理之后,剩余节点形成环,再也找不到入度为 0 的节点。此时处理过的节点数小于课程总数,就可以判断无法完成全部课程。

# 解题代码

class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        vector<vector<int>> graph(numCourses);
        vector<int> inDegree(numCourses, 0);

        for (const auto& edge : prerequisites) {
            int course = edge[0];
            int prerequisite = edge[1];
            graph[prerequisite].push_back(course); // 先修课指向后续课
            ++inDegree[course];
        }

        queue<int> courses;
        for (int i = 0; i < numCourses; ++i) {
            if (inDegree[i] == 0) courses.push(i);
        }

        int learned = 0;
        while (!courses.empty()) {
            int current = courses.front();
            courses.pop();
            ++learned;

            for (int next : graph[current]) {
                --inDegree[next]; // current 学完,移除一条前置依赖
                if (inDegree[next] == 0) courses.push(next);
            }
        }

        return learned == numCourses;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 复杂度分析

  • 时间复杂度:O(V + E),V 是课程数,E 是依赖关系数。
  • 空间复杂度:O(V + E),邻接表、入度数组和队列所需空间。

# 其他语言

# Python3

from collections import deque

class Solution:
    def canFinish(self, numCourses, prerequisites):
        graph = [[] for _ in range(numCourses)]
        in_degree = [0] * numCourses
        for course, prerequisite in prerequisites:
            graph[prerequisite].append(course)
            in_degree[course] += 1

        queue = deque(i for i in range(numCourses) if in_degree[i] == 0)
        learned = 0
        while queue:
            current = queue.popleft()
            learned += 1
            for next_course in graph[current]:
                in_degree[next_course] -= 1
                if in_degree[next_course] == 0:
                    queue.append(next_course)
        return learned == numCourses
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 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[] inDegree = new int[numCourses];

        for (int[] edge : prerequisites) {
            graph.get(edge[1]).add(edge[0]);
            inDegree[edge[0]]++;
        }

        Queue<Integer> queue = new ArrayDeque<>();
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) queue.offer(i);
        }

        int learned = 0;
        while (!queue.isEmpty()) {
            int current = queue.poll();
            learned++;
            for (int next : graph.get(current)) {
                if (--inDegree[next] == 0) queue.offer(next);
            }
        }
        return learned == numCourses;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# Go

func canFinish(numCourses int, prerequisites [][]int) bool {
    graph := make([][]int, numCourses)
    inDegree := make([]int, numCourses)
    for _, edge := range prerequisites {
        course, prerequisite := edge[0], edge[1]
        graph[prerequisite] = append(graph[prerequisite], course)
        inDegree[course]++
    }

    queue := []int{}
    for i, degree := range inDegree {
        if degree == 0 { queue = append(queue, i) }
    }

    learned := 0
    for len(queue) > 0 {
        current := queue[0]
        queue = queue[1:]
        learned++
        for _, next := range graph[current] {
            inDegree[next]--
            if inDegree[next] == 0 { queue = append(queue, next) }
        }
    }
    return learned == numCourses
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# JS

var canFinish = function(numCourses, prerequisites) {
    const graph = Array.from({ length: numCourses }, () => []);
    const inDegree = new Array(numCourses).fill(0);

    for (const [course, prerequisite] of prerequisites) {
        graph[prerequisite].push(course);
        inDegree[course]++;
    }

    const queue = [];
    for (let i = 0; i < numCourses; i++) {
        if (inDegree[i] === 0) queue.push(i);
    }

    let learned = 0, front = 0;
    while (front < queue.length) {
        const current = queue[front++];
        learned++;
        for (const next of graph[current]) {
            inDegree[next]--;
            if (inDegree[next] === 0) queue.push(next);
        }
    }
    return learned === numCourses;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 与代码随想录联系

代码随想录的207.课程表讲解了同一套拓扑排序思路,图论主线中的拓扑排序则使用 ACM 输入输出模式完整实现。

做完本题可以继续做210.课程表 II。本题只问能不能完成,210 题还要返回一种合法的学习顺序,队列中节点的出队顺序正好就是答案。

上次更新:: 10/10/2026, 4:20:05 PM

评论

验证登录状态...