编码母题 / 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, [])); // 预期:[]
/*
* 题目:课程表与依赖环检测
* 要求:输入非负整数 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, [])); // 预期:[]
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)。邻接表、入度表与队列共同保存课程和边。