本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
今天回顾了一下数据结构当中的图,于是上leetcode刷了一波相关题型。记录这道经典课程表题
题目:
现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。- 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1]
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。
加上几个事例+提示
生活的例子
- 先穿内裤再穿裤子,先穿打底再穿外套,先穿衣服再戴帽子,约定俗成
- 内裤外穿、光着身子戴帽等,都会有点奇怪
- 约束我们的一条条 先后规则,能否转成一串顺序行为——衣服是一件件穿的
引入有向图 描述依赖关系
- 示例:n = 6,先决条件表:[ [3, 0], [3, 1], [4, 1], [4, 2], [5, 3], [5, 4] ]
- 0, 1, 2 没有先修课,可以直接选。其余的,都要先修 2 门课
- 我们用 有向图 描述这种 依赖关系 (做事的先后关系):
- 把这样一个 有向无环图 变成 线性的排序 就叫 拓扑排序
- 有向图 中有 入度 和 出度 概念:
- 如果存在一条有向边 A --> B,则这条边给 A 增加了 1 个出度,给 B 增加了 1 个入度
- 所以顶点 0、1、2 的 入度为 0。 顶点 3、4、5 的 入度为 2
每次只能选你能上的课
- 每次只能选 入度为 0 的课,因为它不依赖别的课
- 假设选了 0,导致 依赖 0 的课的入度减小,课 3 的入度由 2 变 1
- 接着选 1,导致课 3 的入度变 0,课 4 的入度由 2 变 1
- 接着选 2,导致课 4 的入度变 0,当前 3 和 4 入度为 0
- 继续选 入度为 0 的课 …… 直到选不到 入度为 0 的课
形似 树的BFS
- 起初让 入度为 0 的课 入列
- 然后逐个出列,课出列 即 课被选 ,并 减小相关课的入度
- 判定是否有课的入度新变为 0,安排入列、再出列……
- 直到没有 入度为 0 的课 可入列……
BFS 前的准备工作
- 我们关心 课的入度 —— 该值要被减,要被监控
- 我们关心 课之间的依赖关系 —— 选这门课会减小哪些课的入度
- 因此我们需要合适的数据结构,去存储这些关系
入度数组 和 邻接表
- 课号是 0 到 n - 1,作为索引,值为入度。遍历先决条件表,求出每门课的初始入度
- 用哈希表记录 依赖关系 (也可以用 邻接矩阵 ,但它有点大)
- key: 课的编号
- value: 依赖它的后续课程
BFS 思路
- queue 队列中始终是【入度为 0 的课】在里面流动
- 选择一门课,就让它 出列,同时 查看哈希表,看它 对应哪些后续课
- 将这些后续课的 入度 - 1,如果有 减至 0 的,就将它 推入 queue
- 不再有新的入度 0 的课入列 时,此时 queue 为空,退出循环
var findOrder = (numCourses, prerequisites) => {
let inDegree = new Array(numCourses).fill(0) // 初始化入度数组
let graph = {} // 哈希表
for (let i = 0; i < prerequisites.length; i++) {
inDegree[prerequisites[i][0]]++ // 构建入度数组
if (graph[prerequisites[i][1]]) { // 构建哈希表
graph[prerequisites[i][1]].push(prerequisites[i][0])
} else {
let list = []
list.push(prerequisites[i][0])
graph[prerequisites[i][1]] = list
}
}
let res = [] // 结果数组
let queue = [] // 存放 入度为0的课
for (let i = 0; i < numCourses; i++) { // 起初推入所有入度为0的课
if (inDegree[i] === 0) queue.push(i)
}
while (queue.length) { // 没有了入度为0的课,没课可选,结束循环
let cur = queue.shift() // 出栈,代表选这门课
res.push(cur) // 推入结果数组
let toEnQueue = graph[cur] // 查看哈希表,获取对应的后续课程
if (toEnQueue && toEnQueue.length) { // 确保有后续课程
for (let i = 0; i < toEnQueue.length; i++) { // 遍历后续课程
inDegree[toEnQueue[i]]-- // 将后续课程的入度 -1
if (inDegree[toEnQueue[i]] == 0) { // 一旦减到0,让该课入列
queue.push(toEnQueue[i])
}
}
}
}
return res.length === numCourses ? res : [] // 选齐了就返回res,否则返回[]
};
总结
-
解决拓扑排序问题
- 根据依赖关系,构建邻接表、和入度数组
- 选取入度为 0 的数据,根据邻接表,减小依赖它的数据的入度
- 找出新入度为 0 的数据,重复第 2 步
- 直至所有数据的入度为 0,得到排序,如果还有数据的入度没有变到 0,说明存在环形依赖
-
和 传统BFS 不一样的地方
- 传统BFS:把出列节点的下一层子节点推入 queue,不加甄别
- 拓扑排序:实施甄别和监控,新入度为 0 的先推入 queue