拓扑排序解课程表

拓扑排序解课程表

可能很多小伙伴在学习数据结构的时候,到图这一章感觉难度突然就上来了。里面的算法明显复杂的多,我刚开始学习的时候也是一头雾水,当时偷懒就划水过去了,直到面临找工作才发现之前偷得懒都是欠的债,总是要还的。

所以我总结了一下拓扑排序的基本思路,顺便拿leetcode上两道题练一下手。

207. 课程表

210. 课程表 II

1.什么是拓扑排序

拓扑排序,这个名字听起来牛逼哄哄,但我个人以为它是图论几个经典算法里面最简单最容易理解的,所以大家不用紧张,看完此文就能对它有一个基本认识了。

先说定义:在一个有向无环图中,将图中节点排列成线性的序列,要求中任意一对顶点u和v,如果边< u,v >(即从u到v有边),则序列中节点u排在节点v的前面。

举个例子来看(图片来自百度百科),比如在下图中,我们输出的拓扑序列为:V1->V3->V4->V6->V5->V7。可以看到图中每条边的起点在序列中一定位于边终点的前面。比如V1到V3有边,那么序列中V1位于V3之前。那可能就会有人问,为什么V3在V4前面?这里他们两个谁在前面都是正确的,因为拓扑排序的结果本身可能是不唯一的,只要满足定义的要求即可。最后我们管这种序列叫做满足拓扑次序的序列。
在这里插入图片描述

接下来我们要说说拓扑排序到底有啥用。

第一个主要用途举例:在一个操作系统中要执行若干作业,某些作业需要用到其他作业的执行结果,所以一些作业一定要先于另一些执行,否则可能造成资源浪费甚至死锁。你想让操作系统知道应该先执行那个后执行那个,怎么办呢?我们可以将作业看成节点,他们的依赖关系看成边,然后拓扑排序,收工。

其实在工程中,我们把这种顶点表示活动、边表示活动间先后关系的有向图称做顶点活动图,使用拓扑排序可以有效的找到各种活动的优先序列。

第二个主要用途:发现图中是否存在环路。回顾定义,在有向无环图中可以拓扑排序。如果有环怎么办呢?我们发现如果存在环路是无法找到一个满足拓扑次序的序列的。这样我们发现如果能够进行拓扑排序,则图中没有环,如果不能,就是有环图。我刚才好像说到死锁,死锁不就是一个环路嘛。其实在死锁检测的过程中也可以用到类似的思想。

2.讲讲算法

我们来讲讲算法的实现细节,首先我们需要两个集合,一个用来存放排好序的节点,初始为空,我们姑且叫它集合A。另一个是存放尚未排序的节点,初始存放全部节点,代号集合B。接下来我们循环执行一下两个操作,直到所有集合B中节点都进入集合A。

1.遍历集合B,找到一个入度为0的节点,将其取出,并放入集合A。如果找不到入度为0的点,返回False,表示无法拓扑排序。

2.从网络(就是原来的图)中删除该节点所有出边

3.练练手

正好leetcode上有相关的问题,我们来用拓扑排序做两题,这样大家能对这个算法有更深刻的理解。

我们先看第一题:207.课程表,题目描述如下图:

在这里插入图片描述

这道题就应用了我们之前讲的拓扑排序的第二个作用:判断图中是否存在环路。如果课程的依赖关系存在环路,那么你将永远无法完成所有课程的学习。那么我们将课程作为节点,依赖关系作为边,比如:如果课程A依赖课程B,我们连一条从B到A的边。以此构建一个图,在这个图上执行拓扑排序算法即可。

这个题第一个难点在于用什么数据结构来保存这张图,当然我们可以用邻接矩阵或邻接表的形式。这边在python里面我们可以考虑用哈希表来存储,其实也相当于一个邻接表啦,这样比邻接矩阵效率更高些。不过我们还需要一个额外的哈希表来存放每个节点的入度,方便查阅。

我们来看看代码实现,思路全在注释里:

	def canFinish(self, numCourses, prerequisites):
        """
        :type numCourses: int
        :type prerequisites: List[List[int]]
        :rtype: bool
        """
		# 用两个哈希表分别存放每个节点的入度和邻接表
        InDegree = {}
        edge = {}
		# 把prerequisites中数据存入邻接表,构造成图
        for i in range(numCourses):
            InDegree[i] = 0
            edge[i] = []
        for i in prerequisites:
            InDegree[i[0]]+=1
            edge[i[1]].append(i[0])
        # 设置两个变量用于判断循环结束条件
        Finished = 0		#表示已经上一轮完成排序的节点数量
        Cur_Finish = 0		#表示经过本轮循环之后完成的节点数量
        # 开始拓扑排序
		while(True):
            for i in InDegree.keys():		#遍历所有节点
                if InDegree[i]<=0:			#找出入度为0的节点
                    for j in edge[i]:		#沿着这个节点的出边,找出所有后继节点
                        InDegree[j]-=1		#后继节点入度减一
                    InDegree.pop(i)			#删除该节点
                    Cur_Finish+=1
            # 判断是否可以结束循环,注意Cur_Finish和Finished两个变量的操作
            if Cur_Finish==numCourses:
                return True
            # 如果某次遍历后没有找到新的可删除节点,说明图中成环。
            if Cur_Finish != Finished:
                Finished = Cur_Finish
            else:
                return False

看吧,其实思路还是挺清晰的,代码也不长。我们再来练一道。210. 课程表 II。

这道题基本上和上一道完全一样的,只不过要求我们输出排序结果的序列,那我们在循环中加一行就可以了。

	def findOrder(self, numCourses, prerequisites):
        """
        :type numCourses: int
        :type prerequisites: List[List[int]]
        :rtype: bool
        """
		# 专门留出一个列表存放结果
		res = []
		# 用两个哈希表分别存放每个节点的入度和邻接表
        InDegree = {}
        edge = {}
		# 把prerequisites中数据存入邻接表,构造成图
        for i in range(numCourses):
            InDegree[i] = 0
            edge[i] = []
        for i in prerequisites:
            InDegree[i[0]]+=1
            edge[i[1]].append(i[0])
        # 设置两个变量用于判断循环结束条件
        Finished = 0		#表示已经上一轮完成排序的节点数量
        Cur_Finish = 0		#表示经过本轮循环之后完成的节点数量
        # 开始拓扑排序
		while(True):
            for i in InDegree.keys():		#遍历所有节点
                if InDegree[i]<=0:			#找出入度为0的节点
                    for j in edge[i]:		#沿着这个节点的出边,找出所有后继节点
                        InDegree[j]-=1		#后继节点入度减一
                    InDegree.pop(i)			#删除该节点
                    Cur_Finish+=1
					res.append(i)			#就这一行不一样
					
            # 判断是否可以结束循环,注意Cur_Finish和Finished两个变量的操作
            if Cur_Finish==numCourses:
                return res
            # 如果某次遍历后没有找到新的可删除节点,说明图中成环。
            if Cur_Finish != Finished:
                Finished = Cur_Finish
            else:
                return []

4.最后总结

我觉得拓扑排序真正的难点在于,你能在实际问题中想到这种方法,并且设计合适的数据结构和代码实现出来,这也是最让人有成就感的地方。

拓扑排序作为很基础的算法还是要掌握的,它其实还有BFS和DFS的实现方法,我还没有具体研究过,我会抽时间好好看看,学无止境嘛。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值