「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」
前言
前端算法系列是我对算法学习的一个记录, 主要从常见算法、数据结构、算法思维、常用技巧几个方面剖析学习算法知识, 通过LeetCode平台实现刻意练习, 通过掘金和B站的输出来实践费曼学习法, 我会在后续不断更新优质内容并同步更新到掘金、B站和Github, 以记录学习算法的完整过程, 欢迎大家多多交流、点赞、收藏, 让我们共同进步, daydayup👊
目录地址:目录篇
相关代码地址: Github
相关视频地址: 哔哩哔哩-百日算法系列
一、什么是拓扑排序
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。
简单来说, 拓扑排序就是在不破环节点 先后顺序 的前提下, 将一个 有向无环图(DAG)拉成一条链的过程.
在这个过程中, 我们不能破环原有的先后顺序, 例如3到2是有明确的指向关系的, 我们不能将2放在3的前面.
二、如何实现拓扑排序
实现拓扑排序一般有深搜(DFS)和广搜(BFS)两种方案, 其中比较经典的就是Kahn算法, 它是一种广搜算法, 利用队列实现.
在实现算法之前我们还需要了解一些 图算法 相关的知识:
度: 就是一个顶点相关联的边的数量
入度: 就是以某个顶点开始, 以当前顶点结束, 边的数量. (也就是有几个箭头指向该顶点)
出度: 就是以某个顶点结束, 以当前顶点开始, 边的数量. (也就是从该顶点延伸出几条线)
在明白了入度和出度的意思之后, 我们就可以开始梳理我们的思路
-
我们将
入度为0的顶点放到队列中 -
更新图中顶点的度, -
因为操作的图是一个有向无环图, 在操作的过程中我们永远都能找到入度为0的顶点, 重复以上操作
/*
图的结构
1.邻接矩阵(二维数组)
2.邻接表(可变长度数组、链表)
*/
// leetcode 207.课程表
// https://leetcode-cn.com/problems/course-schedule/
/**
* @param {number} numCourses
* @param {number[][]} prerequisites
* @return {boolean}
*/
var canFinish = function(numCourses, prerequisites) {
let quenu = []
let indeg = new Array(numCourses).fill(0)
let echart = new Array(numCourses).fill(1).map(() => [])
// 初始化 度 和 图
for(let x of prerequisites) {
// x[0] 表示终点
// x[1] 表示起点
indeg[x[0]] += 1
echart[x[1]].push(x[0])
}
// 初识化 队列
for(let i=0; i<numCourses; i++) {
if (indeg[i] === 0) quenu.push(i)
}
// 循环遍历
let cnt = 0
let ans = new Array(numCourses)
while(quenu.length) {
let front = quenu.shift()
ans[cnt++] = front
for(let x of echart[front]) {
indeg[x] -= 1
if (indeg[x] === 0) {
quenu.push(x)
}
}
}
return numCourses === cnt
};
三、拓扑排序有什么用
1.求拓扑序
对存在一定
依赖关系的数据进行排序
获取拓扑顺序是拓扑排序最常用的功能, 它把一个有向无环图转化为一个线性序列.
拓扑排序同队列与栈一样, 应用十分广泛, 比如, 你一会要下楼去便利店买点东西, 去快递站拿个快递. 这里我们可以将其拆分为几个步骤:
- 下楼
- 去便利店
- 买东西
- 去快递站
- 拿快递
- 上楼
在这些步骤中, 是存在一定的依赖关系的, 比如我们要买东西首先需要到达便利店, 我们要拿快递必须先到达快递站
此外, 诸如向 启动优化, 软件安装, 课程学习等...各种问题的本质都是对存在一定依赖关系的数据进行一个排序, 从而得到最终的一个拓扑序.
2.是否有环判断
因为拓扑排序是针对 有向无环图(DAG)的排序, 我们可以通过是否能够得出拓扑排序来判断是否有环. 当我们使用广搜出现以下情况时, 我们就可以断定当前是有环的.
队列为空 && 得到的拓扑排序的点的数量 < 总的顶点数量