小禹禹,你们好呀!今天是五四青年节,我们的节日,小禹禹过得可好,景禹从昨天到今天,辛辛苦苦给大家写了这篇文章,也算是对五四青年运动的祭奠,“青年兴则国家兴,青年强则国家强。”希望今日之小禹禹胜过昨日之小禹禹,每一天都变强!

今天景禹给你们谈一谈拓扑排序,乍一听上去,感觉很高大上,但她的确很高大上,所以一起征服她吧!在正式介绍拓扑排序之前,我们一起看一看必备基础。

拓扑排序基础篇

总觉得书上的概念有点欠缺生动,但还是需要这些基础的概念作为支撑。

什么是有向无环图?

一个 无环的有向图 称为 有向无环图(Directed Acycline Graph),简称 DAG 图。(这不等于没说吗?)所以直接看图。

图中最左边的是有向树,中间的是有向无环图,最右则的是有向图(因为图中BED三个顶点之间构成一个有向环,ACEB也存在环路)。

什么是 “活动” ?

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

什么是AOV网?

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

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

什么是拓扑序列?

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

什么是拓扑排序呢?

所谓的拓扑排序,其实就是对一个有向无环图构造拓扑序列的过程。当然这里的说法不够正式,也是为了理解方便,拓扑排序的官方定义是这样的:

由某个集合上的一个偏序得到该集合上的一个全序的操作过程称为拓扑排序。

那什么是偏序,什么又是全序呢,我希望小禹禹上课的时候好好听《离散数学》老师讲课,你就可以严格地按照数学的方式对拓扑排序进行推导了。题外话,《离散数学》真的很重要,和所有计算机基础都是一样重要,景禹研究生的硕士论文就是用离散数学的知识和数据结构中的并查集解决的,还望各位小禹禹珍重奥,景禹不会骗你的,就像我说的 “景禹想带你拥有更美好的生活,化作你的一把伞”,有些经验教训没有必要自己尝试一番才领悟。

接下来,景禹带你一起走进拓扑排序的大门。

拓扑排序思想篇

所谓思想篇,就是考试中常考的内容,所以参加考试的朋友最好跟着景禹的讲解自己在纸上画一画,景禹相信对你的考试定有帮助。

拓扑排序的算法步骤很简单,就是两步:

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

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

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

为了清楚地理解拓扑排序思想,我们分别采用有向无环图和有向有环图进行举例说明。

有向无环图

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

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

之后的步骤就是重复一二两步,我们接着看。

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

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

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

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

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

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

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

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

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

有向有环图

其实过程和之前类似,只是给大家提供一个思路,如果面试官问你,如何判断一个有向图中是否存在环时,你应该第一反应想到的就是拓扑排序,为什么拓扑排序可以判断一个有向图中是否存在环呢?我们看栗子。

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

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

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

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

第五步:在有向图中选择一个没有前驱的顶点并输出;发现当前图不存在无前驱的顶点,但拓扑序列中并未输出所有的顶点,所以剩下的顶点构成了环,也证明了该有向图存在环。

拓扑排序实现篇

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

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

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

图的邻接表表示。

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

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

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

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

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

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

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

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

有了上面的基础,我们再来看代码就轻松多了。

// 拓扑排序算法
// 若GL无回路,则输出拓扑排序序列并返回OK,否则返回ERROR
Status TopologicalSort(GraphAdjList GL)
{EdgeNode *e;int i, k, gettop;int top = 0;  // 用于栈指针下标索引int count = 0;  // 用于统计输出顶点的个数int *stack;   // 用于存储入度为0的顶点stack = (int *)malloc(GL->numVertexes * sizeof(int));for( i=0; i < GL->numVertexes; i++ ){if( 0 == GL->adjList[i].in ){stack[++top] = i; // 将度为0的顶点下标入栈}}while( 0 != top ){gettop = stack[top--]; // 出栈printf("%d -> ", GL->adjList[gettop].data);count++;    for( e=GL->adjList[gettop].firstedge; e; e=e->next ){k = e->adjvex;// 注意:下边这个if条件是分析整个程序的要点!// 将k号顶点邻接点的入度-1,因为他的前驱已经消除// 接着判断-1后入度是否为0,如果为0则也入栈if( !(--GL->adjList[k].in) ) {stack[++top] = k;}}}if( count < GL->numVertexes ) // 如果count小于顶点数,说明存在环{return ERROR;}else{return OK;}
}

时间复杂度分析

对于包含n个顶点和 e 条弧的有向图而言,求各个顶点的入度的时间复杂度为 ;遍历所有顶点,查找入度为零并入栈的时间复杂度为 ;在拓扑排序过程中,若有向图无环,则每个顶点入一次栈,出一次栈,入度减 1 的操作在 WHILE 语句中总共执行了 e 次,所以中的时间复杂度为 .

拓扑排序实战篇

LeetCode 207. 课程表 (course-schedule))

题目描述

你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1 。

在选修某些课程之前需要一些先修课程。例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]

给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?

示例

示例 1:

输入: 2, [[1,0]] 输出: true 解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。

示例 2:

输入: 2, [[1,0],[0,1]] 输出: false 解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。

题解:

请你判断是否可能完成所有课程的学习?  你的第一反应是否想到题目就相当于判断一个有向图中是否有环呢?我相信小禹禹一定想到了,比如示例二的输入就表示有向图中存在环,所以不能完成所有课程,如下图形式:

而示例一中就不存在环,即可以完成所有课程,如下图所示:

当然真实的输入可能比给的示例复杂的多,但是不论多么复杂,只需要使用拓扑排序排查一遍即可,这就是算法的魅力。

实现起来也很简单,大家可以将上面的C语言代码稍加改动即可,这里景禹给大家提供利用栈实现的Java代码:

class Solution {public boolean canFinish(int numCourses, int[][] prerequisites) {int[] indegrees = new int[numCourses];List<List<Integer>> adjacency = new ArrayList<>();LinkedList<Integer> stack = new LinkedList<>();for(int i = 0; i < numCourses; i++)adjacency.add(new ArrayList<>());for(int[] cp : prerequisites) {indegrees[cp[0]]++;adjacency.get(cp[1]).add(cp[0]);}for(int i = 0; i < numCourses; i++)if(indegrees[i] == 0) stack.add(i);while(!stack.isEmpty()) {int pre = stack.pollLast();numCourses--;for(int cur : adjacency.get(pre))if(--indegrees[cur] == 0) stack.add(cur);}return numCourses == 0;}
}

还有一道题目,LeetCode 210. 课程表 II (course-schedule)),有兴趣的小禹禹可以去试一试,其实只要在 207 的解法上增加一个输出即可,景禹就不在这里废话了,但是景禹要给大家提供一种输出所有拓扑序列的代码,可供学有余力的小禹禹抽空自己调试运行理解。

得到一个有向无环图的所有拓扑序列:

import java.util.*; class Graph { int V; List<Integer> adjListArray[]; public Graph(int V) { this.V = V; @SuppressWarnings("unchecked") List<Integer> adjListArray[] = new LinkedList[V]; this.adjListArray = adjListArray; for (int i = 0; i < V; i++) { adjListArray[i] = new LinkedList<>(); } } public void addEdge(int src, int dest) { this.adjListArray[src].add(dest); } private void allTopologicalSortsUtil(boolean[] visited, int[] indegree, ArrayList<Integer> stack) { boolean flag = false; for (int i = 0; i < this.V; i++) { if (!visited[i] && indegree[i] == 0) { visited[i] = true; stack.add(i); for (int adjacent : this.adjListArray[i]) { indegree[adjacent]--; } allTopologicalSortsUtil(visited, indegree, stack); visited[i] = false; stack.remove(stack.size() - 1); for (int adjacent : this.adjListArray[i]) { indegree[adjacent]++; } flag = true; } } if (!flag) { stack.forEach(i -> System.out.print(i + " ")); System.out.println(); } } public void allTopologicalSorts() { boolean[] visited = new boolean[this.V]; int[] indegree = new int[this.V]; for (int i = 0; i < this.V; i++) { for (int var : this.adjListArray[i]) { indegree[var]++; } } ArrayList<Integer> stack = new ArrayList<>(); allTopologicalSortsUtil(visited, indegree, stack); } public static void main(String[] args) { Graph graph = new Graph(6); graph.addEdge(5, 2); graph.addEdge(5, 0); graph.addEdge(4, 0); graph.addEdge(4, 1); graph.addEdge(2, 3); graph.addEdge(3, 1); System.out.println("All Topological sorts"); graph.allTopologicalSorts(); }
}

总结

拓扑排序在实际的生产中很常见,最经典的例子就是课程表,当然还可以进行代码的依赖关系处理等等,此外,拓扑排序还是之后要将的关键路径的基础,所以希望各位小禹禹好好学习,有所进步。

记得点赞,留言,夸夸我呀呀呀~~

推荐文章

  • 图解:什么是图?(以“图”话图)

  • 动画解析:图的遍历方式有哪些?

  • 图解:最短路径之迪杰斯特拉算法

作者:景禹,一个追求极致的共享主义者,想带你一起拥有更美好的生活,化作你的一把伞。

图解:什么是拓扑排序?相关推荐

  1. 拓扑排序Kahn算法

    拓扑排序 介绍 思路 操作过程 完美图解 代码模板 介绍 拓扑排序,整体是给出n个事件先后关系,来确定n个事件最终的先后关系 思路 很好理解 操作过程 我们可以理解成一个有向图如果x事件在y事件的前面 ...

  2. 图解Topo拓扑排序

    往期文章目录 [干货满满!]图解Dijkstra迪杰斯特拉 [上篇文章!]手撕Floyd弗洛伊德 目录 往期文章目录 前言 一.Topo排序不是排序 二.AOV网和Topo序列 1.AOV网的概念 2 ...

  3. 图解Topo拓扑排序 例题洛谷P4017 最大食物链计数

    适用条件:无环图 输出结果:使得每个节点,以它为终点的起点节点排都在其前面 作用:递推保证前面的节点都已经被使用过 实现方法:从没有入边的节点开始,输出并从其他节点中删去自己,重复此步骤直到所有节点都 ...

  4. 图解:有向环、拓扑排序与Kosaraju算法

    25张图详解有向环.拓扑排序与Kosaraju算法.有向环如何检测?拓扑排序的原理?Kosaraju算法又是如何得到的?本文告诉你答案 首先来看一下今天的内容大纲,内容非常多,主要是对算法思路与来源的 ...

  5. aov建立Java模拟,数据结构之---C语言实现拓扑排序AOV图

    //有向图的拓扑排序 //杨鑫 #include #include #include #define MAX_NAME 3 #define MAX_VERTEX_NUM 20 typedef int ...

  6. 判断图有无环_浅谈什么是图拓扑排序

    1 引言   在工程实践中,一个工程项目往往由若干个子项目组成.这些子项目间往往有两种关系:   (1) 先后关系,即必须在某个项完成后才能开始实施另一个子项目.   (2) 子项目间无关系,即两个子 ...

  7. 用C语言编程实现拓扑排序,拓扑排序(一)之 C语言详解

    本章介绍图的拓扑排序.和以往一样,本文会先对拓扑排序的理论知识进行介绍,然后给出C语言的实现.后续再分别给出C++和Java版本的实现. 拓扑排序介绍 拓扑排序(Topological Order)是 ...

  8. 图论算法—图的拓扑排序介绍和Kahn算法原理解析以及Java代码的实现

    详细介绍了图的拓扑排序的概念,然后介绍了求拓扑序列的算法:Kahn算法的原理,最后提供了基于邻接矩阵和邻接表的图对该算法的Java实现. 阅读本文需要一定的图的基础,如果对于图不是太明白的可以看看这篇 ...

  9. 拓扑排序之java实现_有向图和拓扑排序Java实现

    package practice; import java.util.ArrayDeque; import java.util.Iterator; import java.util.Stack; pu ...

  10. 浅谈什么是图拓扑排序

    1 引言   在工程实践中,一个工程项目往往由若干个子项目组成.这些子项目间往往有两种关系:   (1) 先后关系,即必须在某个项完成后才能开始实施另一个子项目.   (2) 子项目间无关系,即两个子 ...

最新文章

  1. mysql router安装教程_MySQL Router 安装与配置
  2. SparkSQL之thriftserver/beeline的使用
  3. svg放大缩小拖动_Day2 三种图表技术SVG、Canvas、WebGL 3D比较
  4. text html template判断,template-web中循环 判断 计算
  5. python装饰器带参数函数_python带参数装饰器的两种写法
  6. 【二分图】【最大匹配】【匈牙利算法】bzoj1191 [HNOI2006]超级英雄Hero
  7. bootstrap table 服务器端分页--ashx+ajax
  8. python_列表_常用操作
  9. SPSS问卷或量表调查研究需要多少份或要求多大的样本量?【SPSS 062期】
  10. [图灵程序设计丛书].流畅的Python.revise11.pdf
  11. 2017 年全国大学生电子设计竞赛(本科组)题目√
  12. 贪吃蛇游戏-小程序游戏
  13. 使用javascript实现点击复制到剪贴板
  14. 【屏幕适配】像素密度 分辨率 dp px的关系
  15. SAP 销售订单冻结无法交货问题
  16. 想从事人工智能方面,需要自学什么?
  17. Linux(CS-Notes)
  18. 美国旧金山的大学之东城西语
  19. html5查看swf视频格式,如何使用Videojs播放.swf格式文件?
  20. 链路分析(Link analysis)

热门文章

  1. ERROR: sdl2 requested but not found
  2. Java多线程---Phaser
  3. #!/bin/bash 和 #!/usr/bin/env bash 的区别
  4. 窥见数字未来“天机”,IBM正式发布2019十大行业洞察
  5. 经典点云配准算法:迭代最近点算法ICP(Iterative Closest Point)
  6. mysql无法创建partition_mysql的partition分区
  7. 纯干货:Linux抓包命令集锦
  8. banner设圆角_如何规范运营Banner设计?
  9. 有效的数据治理,元数据该怎么管?
  10. PAT 甲级 1020. Tree Traversals