编码母题 / P0 / 经典算法 / 10

课程表与依赖环检测

重要程度必练难度中等预计完成30–40 分钟

题目是什么

输入非负整数 numCourses(课程编号 0 到 numCourses-1)和先修关系数组 prerequisites;每项 [course, prerequisite] 的编号均有效,表示先学 prerequisite 才能学 course。返回一种包含全部课程且满足依赖的顺序;有环时返回 [],重复关系视为一条边,零门课程返回 []。

输入输出示例

numCourses=3、prerequisites=[[1,0],[2,1]] 返回 [0,1,2];[[1,0],[0,1]] 形成环,返回 []。

练习起手式与验证

/*
 * 题目:课程表与依赖环检测
 * 要求:输入非负整数 numCourses(课程编号 0 到 numCourses-1)和先修关系数组 prerequisites;每项 [course, prerequisite] 的编号均有效,表示先学 prerequisite 才能学 course。返回一种包含全部课程且满足依赖的顺序;有环时返回 [],重复关系视为一条边,零门课程返回 []。
 * 示例:numCourses=3、prerequisites=[[1,0],[2,1]] 返回 [0,1,2];[[1,0],[0,1]] 形成环,返回 []。
 */
function findCourseOrder(numCourses, prerequisites) {}
console.log(findCourseOrder(2, [[1, 0]])); // 预期:[0, 1]
console.log(findCourseOrder(2, [[1, 0], [0, 1]])); // 预期:[]
console.log(findCourseOrder(3, [[1, 0], [1, 0], [2, 1]])); // 预期:[0, 1, 2]
console.log(findCourseOrder(0, [])); // 预期:[]

解题思路与实现

展开解题思路

Kahn 算法先取入度为零的节点,移除边后不断产生新的可执行节点。

function findCourseOrder(numCourses, prerequisites) {
  if (!Number.isInteger(numCourses) || numCourses < 0)
    throw new RangeError("invalid numCourses");
  const graph = Array.from({ length: numCourses }, () => []);
  const indegree = Array(numCourses).fill(0);
  const edges = new Set();
  for (const pair of prerequisites) {
    if (!Array.isArray(pair) || pair.length !== 2)
      throw new TypeError("invalid prerequisite");
    const [course, prerequisite] = pair;
    if (
      ![course, prerequisite].every(
        (v) => Number.isInteger(v) && v >= 0 && v < numCourses,
      )
    )
      throw new RangeError("course out of range");
    const key = `${prerequisite}:${course}`;
    if (edges.has(key)) continue;
    edges.add(key);
    graph[prerequisite].push(course);
    indegree[course] += 1;
  }
  const queue = [];
  for (let i = 0; i < numCourses; i += 1) if (indegree[i] === 0) queue.push(i);
  const order = [];
  for (let head = 0; head < queue.length; head += 1) {
    const course = queue[head];
    order.push(course);
    for (const next of graph[course]) {
      indegree[next] -= 1;
      if (indegree[next] === 0) queue.push(next);
    }
  }
  return order.length === numCourses ? order : [];
}

// 三组以上可直接运行的验证。
console.log(findCourseOrder(2, [[1, 0]])); // 预期:[0, 1]
console.log(findCourseOrder(2, [[1, 0], [0, 1]])); // 预期:[]
console.log(findCourseOrder(3, [[1, 0], [1, 0], [2, 1]])); // 预期:[0, 1, 2]
console.log(findCourseOrder(0, [])); // 预期:[]

时间复杂度:O(V+E)。每个课程和每条去重后的依赖边最多处理一次。

空间复杂度:O(V+E)。邻接表、入度表与队列共同保存课程和边。

下载原始 JS 文件