算法数据结构 | 图论基础算法——拓扑排序
今天是算法和数据结构专题的第32篇文章,我们来聊聊拓扑排序的问题。
拓扑排序是图论当中一个非常简单也非常常用的算法,它有很多的功能。它可以用来检测有向图当中是否存在环,也可以用来解决存在依赖的调度问题。下面我们就来看看这个算法的庐山真面目吧。
算法场景
拓扑排序是英文音译,它的英文原文是Topological Sorting,是一个比较抽象的概念,没有很信达雅的翻译。它指的是一个DAG(Directed Acyclic Graph)即有向图所有顶点满足一定条件的线性序列。也就是说图中的这些顶点的排序之间存在一定的逻辑结构和顺序结构,是这两种拧在一起的一个抽象的概念。
那么这些顶点的排序之间应该满足什么样的条件呢?
其实很简单只有两点:
- 每个点都只出现一次,这个不用解释了
- 如果存在一条从A指向B的边,那么A点在序列中出现的顺序应该在B点之前。关于这点简单解释一下,我们可以把有向边看成一条调度上的依赖。A指向B,即B依赖A,那么显然A应该出现在B前面。就好像淘米 -> 煮饭,我们应该先淘米才能煮饭,煮饭依赖淘米。
比如上图当中1 2 4 3 5就是一个合法的拓扑排序,这个序列满足上面两条性质。
算法原理
那么我们怎么得到这个拓扑排序呢?
其实原理非常简单,就是一个数组的事情。首先,我们用一个数组记录每一个点的入度。所谓的入度也就是有多少点指向它,比如上图当中1号点的入度为0,4号点的入度为2,因为它有1和2两个点指向它。
我们要做的就是根据入度一个点一个点的选择,根据前面说的性质,如果一个点的入度不为0,那么它显然不能被选择。因为至少还有一个点应该在它的前面,既然如此,反过来说也就是我们只能选择入度为0的点。如果所有点的入度都不为0呢?不要问,图中肯定有环,不然一定可以找到一个入度为0的点。关于这点有严谨的证明,但我们也没必要证明了,仔细想想就能想明白。
我们选中了入度为0的点之后呢?之后我们需要把它连出去的边全部删掉,我们一样从调度依赖来思考。比如我们想要做寿司也想要做饭团,这两者都依赖于煮饭。现在饭煮好了,这两者的依赖已经完成了,它们应该不受任何限制了。所以我们要把煮饭连向做饭团和做寿司的边去掉,也就是把依赖去掉。去掉边体现在做饭团和做寿司的入度减一,也就是它们上游的依赖少了一个。
整个流程串起来就是拓扑排序的算法了,怎么样是不是很简单呢?
但是还有一个小问题,根据这样我们得到的序列是唯一的吗?如果存在多个入度为0的点怎么办,我们该选哪一个?
显然拓扑排序的情况可能是不唯一的,但是我们是否要获取所有的情况这一点就要根据实际使用的情况来确定了,一般来说我们只需要一个合法的序列就可以了,如果需要得到所有的拓扑排序也不复杂,我们可以将它看成一个带条件限制的搜索问题,搜索一下所有的可能性就OK了。
代码实现
最后,我们来看下代码,真的是史诗级的简单:
paths = [[], [2, 4], [3, 4], [5], [3, 5], []]
indegree = [0 for _ in range(6)]
for u in range(6):
for v in paths[u]:
indegree[v] += 1
topological = set()
for i in range(5):
for u in range(1, 6):
if u not in topological and indegree[u] == 0:
topological.add(u)
for v in paths[u]:
indegree[v] -= 1
print(topological)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 以OpenResty搭建RTB竞价引擎接入层
- 优化Linux bootloader速度的究极之路:从GRUB到EFI Stub
- Linux--nc命令
- Netty之美--I/O模型
- 023.基于IT论坛案例学习Elasticsearch(二):Query高级知识(一)
- 打卡群刷题总结0807——验证二叉搜索树
- 打卡群刷题总结0808——二叉树的层序遍历
- Mybatis高级查询(四):延迟加载
- I/O多路复用器之隐秘的角落
- 打卡群刷题总结0809——二叉树的锯齿形层次遍历
- 简单的ssm整合练手项目:汽车项目
- 在spring-boot中使用pageHelper插件
- 要深入 JavaScript,你需要掌握这 36 个概念
- mybatis-plus实现增删改查
- mybatis-plus代码生成器