什么是拓扑排序?

164 阅读6分钟

基础

什么是有向无环图

一个 无环的有向图 称为 有向无环图(Directed Acycline Graph),简称 DAG 图。

image.png

图中最左边的是有向树,中间的是有向无环图,最右则的是有向图。

什么是 “活动” ?

所有的工程或者某种流程都可以分为若干个小的工程或者阶段,我们称这些小的工程或阶段为“活动”。打个比方,如何把一只大象装到冰箱里,很简单,分三步。第一,打开冰箱门;第二,将大象装进去;第三,关上冰箱门。这三步中的每一步便是一个 “活动” 。

什么是AOV网?

在一个表示工程的有向图中,用 顶点表示活动,用弧表示活动之间的优先关系的有向图 称为顶点表示活动的网(Activity On Vertex Network),简称AOV网。

AOV网中的弧表示活动之间存在的某种制约关系,比如上面说到将大象装入冰箱,必须先打开冰箱门,才能将大象装进去,大象装进去才能关上冰箱门,从而完成我们的任务。还有一个经典的例子那就是选课,通常我们是学了C语言程序设计,才能学习数据结构,这里的制约关系就是课程之间的优先关系。

什么是拓扑序列?

设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列V1,V2...VN满足若从顶点V1到V2有一条路径,则在顶点序列中顶点V1必在顶点V2之前。则我们称这样的顶点序列为一个拓扑序列。

什么是拓扑排序呢?

所谓的拓扑排序,其实就是对一个有向无环图构造拓扑序列的过程。当然这里的说法不够正式,也是为了理解方便,拓扑排序的官方定义是这样的:由某个集合上的一个偏序得到该集合上的一个全序的操作过程称为拓扑排序。

图解算法

拓扑排序的算法步骤就是两步:

(1) 在有向图中选一个没有前驱的顶点且输出之。

(2) 从图中删除该顶点和所有以它为尾的弧。

重复上述两步,直至全部顶点均已输出,或者当前图不存在无前驱的顶点为止,后一种情况说明有向图中存在环。

image.png

第一步:在有向图中选择一个没有前驱的顶点并输出;观察图中的顶点,发现顶点 v1和顶点 v6都是没有前驱的顶。假设我们先输出顶点 v1(当然也可以先输出v6 ,从此处也就可以看出拓扑序列可以有多个)。

image.png

第二步:从图中删除顶点 v1 和所有以它为尾的弧(即上图中的红色有向边)。

image.png

第三步:在有向图中选择一个没有前驱的顶点并输出;图中没有前驱的顶点为 v6 和顶点 v3 (同样的道理,我们可以选择这两个顶点的任何一个,假设我们选择顶点 v6 )。

image.png

第四步:删除顶点 v6  和所有以它为尾的弧

image.png

第五步:在有向图中选择一个没有前驱的顶点并输出;图中没有前驱的顶点为 v4 和顶点 v3 (同样的道理,我们可以选择这两个顶点的任何一个,假设我们选择了顶点 v4 )。

image.png

第六步:删除顶点 v4  和所有以它为尾的弧。

image.png

第七步:在有向图中选择一个没有前驱的顶点并输出;图中没有前驱的顶点为 v3 。

image.png

第八步:删除顶点 v3  和所有以它为尾的弧。

image.png

第九步:在有向图中选择一个没有前驱的顶点并输出;图中没有前驱的顶点为 v2 和  v5 (同样的道理,我们可以选择这两个顶点的任何一个,假设我们选择了顶点 v2 )

image.png

第十步:在有向图中选择一个没有前驱的顶点并输出;图中没有前驱的顶点为  v5 ,选择并输出,此时所有的顶点均已经输出,算法结束,我们就得到了下图中的 一个拓扑序列 ,整个过程便叫做 拓扑排序

image.png

图解实现

针对于拓扑排序思想篇提到的两步操作,我们采用邻接表作为有向图的存储结构,并且在头结点中增加一个存放顶点入度的数组(indegree)。入度为零的顶点即为没有前驱的顶点,删除顶点及以它为尾的弧的操作,则则可换以弧头顶点的入度减1来实现。

为了避免重复检查入度为零的顶点,可另设一个栈暂存所有入度为领的顶点。

为了清晰地呈现拓扑排序的实现,我们还是以上面提到的有向无环图为栗子进行讲解,Step By Step。

图的邻接表表示。

image.png

第一步:遍历所有顶点,将入度为0的顶点入栈,即分别将顶点 v1 和顶点 v6 入栈。

image.png

第二步:弹出栈顶顶点 v6 并输出,遍历顶点 v6 的邻接顶点,即index == 3 和 index == 4 的顶点。将 index == 3 的顶点 v4  的入度减 1 ,发现不为0,则不入栈;将 index == 4 的顶点 v5  的入度减 1 ,发现不为0,则不入栈;

image.png

第三步:弹出栈顶顶点 v1 并输出,然后遍历顶点  v1 的邻接顶点,即 index == 1,index == 2,index == 3的顶点。将 index == 1 的顶点 v2  的入度减 1等于1 ,不为0,则不入栈;将 index == 2 的顶点 v3  的入度减 1等于0 ,则入栈;将 index == 3 的顶点 v4  的入度减 1等于0 ,则入栈;

image.png

第四步:弹出栈顶顶点 v4 并输出,然后遍历顶点  v4 的邻接顶点,即 index == 4的顶点;将 index == 4 的顶点 v5  的入度减 1等于1 ,不为0,则不入栈;

image.png

第五步:弹出栈顶顶点 v3 并输出,然后遍历顶点 v3  的邻接顶点,即 index == 1 和  index == 4 的顶点;将 index == 1 的顶点 v2  的入度减 1等于0 ,则入栈;将 index == 4 的顶点  v5 的入度减 1等于0 ,则入栈;

image.png

第六步:弹出栈顶顶点 v5 并输出,顶点 v5 没有后继顶点;

image.png

第七步:弹出栈顶顶点 v2 并输出,顶点  v2  没有后继顶点;

image.png

此时栈为空且所有的顶点均已输出,故算法终止。