LeetCode 207. 课程表

题目描述

207. 课程表

思路分析

解法一:拓扑排序(BFS,推荐)

核心思路

  • 课程依赖关系构成有向图,能否完成所有课程等价于有向图中是否存在环
  • prerequisites 建图:[a, b] 表示学 a 前必须先学 b,即有向边 b → a
  • 统计每个节点的入度,将所有入度为 0 的节点加入队列(可以直接学的课程)。
  • 每次弹出队首节点,将其所有邻居入度减 1;若某邻居入度变为 0,则入队。
  • 统计处理的节点总数,若等于 numCourses 则无环返回 true,否则返回 false


复杂度分析

  • 时间复杂度:O(V + E),其中 V 为课程数,E 为先修关系数
  • 空间复杂度:O(V + E),其中 V 为课程数,E 为先修关系数
class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        // 建立邻接表和入度数组
        List<List<Integer>> graph = new ArrayList<>();
        int[] inDegree = new int[numCourses];
        for (int i = 0; i < numCourses; i++) {
            graph.add(new ArrayList<>());
        }
        // 构建有向图:pre[1] -> pre[0]
        for (int[] pre : prerequisites) {
            graph.get(pre[1]).add(pre[0]);
            inDegree[pre[0]]++;
        }
        // 将所有入度为 0 的课程加入队列
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }
        int count = 0;
        while (!queue.isEmpty()) {
            int node = queue.poll();
            count++;
            // 将邻居入度减 1,入度为 0 则入队
            for (int next : graph.get(node)) {
                if (--inDegree[next] == 0) {
                    queue.offer(next);
                }
            }
        }
        // 若处理的节点数等于课程总数,说明无环
        return count == numCourses;
    }
}
func canFinish(numCourses int, prerequisites [][]int) bool {
    // 建立邻接表和入度数组
    graph := make([][]int, numCourses)
    inDegree := make([]int, numCourses)
    for i := range graph {
        graph[i] = []int{}
    }
    // 构建有向图:prev -> course
    for _, pre := range prerequisites {
        course, prev := pre[0], pre[1]
        graph[prev] = append(graph[prev], course)
        inDegree[course]++
    }
    // 将所有入度为 0 的课程加入队列
    queue := []int{}
    for i := 0; i < numCourses; i++ {
        if inDegree[i] == 0 {
            queue = append(queue, i)
        }
    }
    count := 0
    for len(queue) > 0 {
        node := queue[0]
        queue = queue[1:]
        count++
        // 将邻居入度减 1,入度为 0 则入队
        for _, next := range graph[node] {
            inDegree[next]--
            if inDegree[next] == 0 {
                queue = append(queue, next)
            }
        }
    }
    // 若处理的节点数等于课程总数,说明无环
    return count == numCourses
}

解法二:DFS 检测环

核心思路

  • 对每个未访问节点进行 DFS,使用三色标记法检测有向图中的环。
  • 0:未访问;1:访问中(在当前 DFS 路径上);2:已完成访问。
  • 若 DFS 中遇到状态为 1 的节点,说明当前路径上存在环,返回 false
  • 所有节点均完成访问且未发现环,则返回 true


复杂度分析

  • 时间复杂度:O(V + E),其中 V 为课程数,E 为先修关系数
  • 空间复杂度:O(V + E),其中 V 为递归栈深度,E 为邻接表大小
class Solution {
    // 0: 未访问,1: 访问中,2: 已完成
    private int[] visited;
    private List<List<Integer>> graph;

    public boolean canFinish(int numCourses, int[][] prerequisites) {
        graph = new ArrayList<>();
        visited = new int[numCourses];
        for (int i = 0; i < numCourses; i++) {
            graph.add(new ArrayList<>());
        }
        // 构建有向图
        for (int[] pre : prerequisites) {
            graph.get(pre[1]).add(pre[0]);
        }
        // 对每个未访问的节点进行 DFS
        for (int i = 0; i < numCourses; i++) {
            if (visited[i] == 0 && hasCycle(i)) {
                return false;
            }
        }
        return true;
    }

    private boolean hasCycle(int node) {
        // 标记为访问中
        visited[node] = 1;
        for (int next : graph.get(node)) {
            // 遇到访问中的节点,说明有环
            if (visited[next] == 1) {
                return true;
            }
            // 对未访问的节点继续 DFS
            if (visited[next] == 0 && hasCycle(next)) {
                return true;
            }
        }
        // 标记为已完成
        visited[node] = 2;
        return false;
    }
}
func canFinish(numCourses int, prerequisites [][]int) bool {
    // 建立邻接表
    graph := make([][]int, numCourses)
    for i := range graph {
        graph[i] = []int{}
    }
    for _, pre := range prerequisites {
        graph[pre[1]] = append(graph[pre[1]], pre[0])
    }
    // 0: 未访问,1: 访问中,2: 已完成
    visited := make([]int, numCourses)

    var hasCycle func(node int) bool
    hasCycle = func(node int) bool {
        // 标记为访问中
        visited[node] = 1
        for _, next := range graph[node] {
            // 遇到访问中的节点,说明有环
            if visited[next] == 1 {
                return true
            }
            // 对未访问的节点继续 DFS
            if visited[next] == 0 && hasCycle(next) {
                return true
            }
        }
        // 标记为已完成
        visited[node] = 2
        return false
    }

    // 对每个未访问的节点进行 DFS
    for i := 0; i < numCourses; i++ {
        if visited[i] == 0 && hasCycle(i) {
            return false
        }
    }
    return true
}

相似题目

题目 难度 考察点
210. 课程表 II 中等 拓扑排序
269. 火星词典 困难 拓扑排序
444. 序列重建 中等 拓扑排序
630. 课程表 III 困难 贪心、堆
802. 找到最终的安全状态 中等 拓扑排序、DFS
851. 喧闹和富有 中等 拓扑排序
1136. 并行课程 中等 拓扑排序
1203. 项目管理 困难 拓扑排序
2115. 从给定原材料中找到所有可以做出的菜 中等 拓扑排序
本文作者:
本文链接: https://hgnulb.github.io/blog/2026/36228668
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!