【拓扑排序】-- 算法原理讲解,及实现拓扑排序,附赠热门例题

文章目录

拓扑排序是图论中处理有向无环图(DAG)的重要算法,用于确定活动执行的合理顺序。本文将详细介绍拓扑排序算法的原理以及拓扑排序的实现。在讲解之前,我们需要先认识了解一下 有向无环图(DAG)AOV 网 (顶点活动图)。

有向无环图:是由顶点和边连接而成的数据结构,图中的边都是有方向的边,无环意味着随意将图中的点提出来,将他们的边组合起来后都是没有环出现的。下图中图一就是有向无环图,图二存在环,不是有向无环图。

下面我们补充两个有向无环图中的概念,入度出度

入度和出度都是针对当前的这个顶点来看的
入度 :指向当前顶点的边
出度 :从当前点指出去的边

eg:上图中,顶点1的入度为0,出度为2;顶点2的入度为1,出度为2,剩下的顶点同理可得,这里不再详细说明。

AOV网:在有向无环图中,用顶点来表示一个活动,用边来表示活动执行的先后顺序的图结构。下面我们举个例子说明一下,如下图(做法多样,图片仅供参考):

图中每个结点代表一个活动,每条边代表活动的执行顺序。比如:在洗菜前的一个条件肯定是得先有菜,在切菜前肯定得先有厨具和菜。像这样的一个流程图就是一个AOV网,AOV网是很具有实际意义的,在现实生活中的应用还是很广泛的。

拓扑排序介绍

在前两个铺垫的知识点讲完后,接下来我们来认识一下什么是拓扑排序。

拓扑排序(Topological sorting)要解决的问题是如何给一个有向无环图的所有节点排序。简而言之就是找到做一件事情的先后顺序,其中对应的事情就是一个一个的活动。继续拿我们上面的青椒炒肉工程图来进行讲解。

拓扑排序原理

①:确定活动的起点,起点有两个选择,一个是准备厨具,另一个是买菜,这两个活动代表的顶点入度均为0 。这里我们选择准备厨具,准备厨具是腌肉和切菜的前提条件 ,图示变化如下(这里可以将图中准备厨具顶点直接移至下方,方便看到过程变化这里未进行移动):

②:现在我们可选的操作只有买菜,买菜顶点的入度为0 ,注意腌肉还有一个前提条件是洗菜(这里菜指可食用物品总称),腌肉顶点的入度为1 ,图示变化如下:

③:如上图所示,接下来我们可选择进行的活动只有洗菜。同理后续的步骤就不再一一进行演示了。若最开始选择买菜,后续的活动执行流程和上面分析的一样。通过分析发现规律,当顶点的入度为0时,当前顶点便可选,不为0时,不可以进行选择

当我们重复上面的操作,一直到最后干饭这个顶点对应的活动被选择,DAG这个图中不存在顶点,我们便完成了本次的拓扑排序。

如何排序呢?通过上面的分析我们不难发现,首先 我们需要找出图中入度为0的顶点,然后依据题目要求,对其进行输出或保存;然后删除与该结点相连的边,重复上面两个操作,直到 DAG 图中没有顶点被边指向。

拓扑排序实现

借助队列,来一次 BFS 即可。具体实现步骤如下:

实验步骤

1.初始化:将图中所有入度为0的顶点加入到队列中

2.当队列不为空的时候:

  • 拿出对头元素,加入到最终结果中(依据题目而定);
  • 删除与该元素相连的边;
  • 判断:与该边相连的点,是否入度变为0,如果入度为0,加入到队列中

重复上面 2 步骤,直到队列中不再存在顶点信息。注意在拿到一道算法题时,不可能参数传递为一个图像,一般传递的都是一个二维数组,我们需要自己将题目已知的二维数组转化为图,建图这个过程,将在下面的例题进行介绍。

接下来我们将具体的实现步骤与实际例题结合起来,便于理解。

拓扑排序例题

例题坐标:

点击跳转拓扑排序例题--课程表

题目描述

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

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。

例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

示例

输入:numCourses = 2, prerequisites = [[1,0]]

输出:true

解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

输入:numCourses = 2, prerequisites = [[1,0],[0,1]]

输出:false

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

例题解析

不知道AOV网和拓扑排序思想的小伙伴拿到这个题可能脑瓜子嗡嗡的。题目的意思是在学习某某课程前,必须要先完成某某课程。这不就是上述拓扑排序的定义,找到事情执行的先后顺序,根据上文,可以写出代码,后续会对代码进行解释。

代码实现

java 复制代码
class Solution {
    public boolean canFinish(int n, int[][] p) {
        int[] in=new int[n];//用于记录元素入度的次数
        Queue<Integer> queue=new ArrayDeque<>();//存入当前本次入度为0的全部元素
        HashMap<Integer,List<Integer>> map=new HashMap<>();//拓扑建图
        for(int i=0;i<p.length;i++){
            int a=p[i][0],b=p[i][1];
            if(!map.containsKey(b)) map.put(b,new ArrayList<>());
            map.get(b).add(a);//建图
            in[a]++;//计算入度
        }
        for(int i=0;i<n;i++){
            if(in[i]==0) queue.offer(i);
        }
        while(!queue.isEmpty()){
            int size=queue.size();
            while(size-->0){
                int cur=queue.poll();
                List<Integer> list=map.getOrDefault(cur,new ArrayList<>());
                for(int x:list){
                    in[x]--;
                    if(in[x]==0) queue.offer(x);
                }
            }
        }
        for(int i=0;i<n;i++){
            if(in[i]!=0) return false;
        }
        return true;
    }
}

代码解析

代码解析part1

java 复制代码
        int[] in=new int[n];//用于记录元素入度的次数
        Queue<Integer> queue=new ArrayDeque<>();//存入当前本次入度为0的全部元素
        HashMap<Integer,List<Integer>> map=new HashMap<>();//拓扑建图
        for(int i=0;i<p.length;i++){
            int a=p[i][0],b=p[i][1];
            if(!map.containsKey(b)) map.put(b,new ArrayList<>());
            map.get(b).add(a);//建图
            in[a]++;//计算入度
        }

在实现拓扑排序之前,我们需要先知道拓扑图的信息。已知题目传递的参数为数组,如何将图和数组建立关联呢?这里的关联是,我们需要知道每个顶点四周边的走向,这里我们可以通过HashMap来实现,具体情况为HashMap<Integer,List<Integer>>,其中key表示先需要执行活动顶点对应的 val 值,value 表示 key 顶点指出方向的顶点对应 val 值。拿示例 1 来看,学习 1 课程前必须要学会 0 课程,箭头的指示方向就应该从 0 指向 1 ,key = 0,value 包含 1, 如下图所示:

现在建图这一步我们已经完成,根据上面的拓扑排序实现具体步骤我们可以知道,我们需要将图中入度为 0 的顶点加入到队列中。在上面的代码实现中,我们定义数组 in

来记录元素入度的次数,因为题目已知课程名字记为 [0~n-1] ,所以说定义数组刚好通过int[] in=new int[n];就可以实现,下标表示课程名字。数组初始值默认为0,上述代码中 b 表示 a 执行的条件,所以说箭头应是 b->a,就应该是 in[a]++

代码解析part2

java 复制代码
        for(int i=0;i<n;i++){
            if(in[i]==0) queue.offer(i);
        }

寻找出入度为 0 的起始顶点,并将其加入到队列中

代码解析part3

java 复制代码
 while(!queue.isEmpty()){
            int size=queue.size();
            while(size-->0){
                int cur=queue.poll();
                List<Integer> list=map.getOrDefault(cur,new ArrayList<>());
                for(int x:list){
                    in[x]--;
                    if(in[x]==0) queue.offer(x);
                }
            }
        }

queue.size()记录当前入度为 0 的顶点的个数,queue.poll()弹出顶点,map.getOrDefault(cur,new ArrayList<>());查找当前顶点是否是其他顶点的前提条件,如若有返回对应的 list 集合,若没有返回空集合,for(int x:list)遍历返回的 list 数组,in[x]--当前订单所指向的所有顶点入度数 -1 , if(in[x]==0) queue.offer(x)随后进行判断,是否入度数为 0 ,若为 0 加入队列。此步骤是在实现拓扑排序步骤中的第二步。

代码解析part4

java 复制代码
        for(int i=0;i<n;i++){
            if(in[i]!=0) return false;
        }

if(in[i]!=0)循环判定,当前所有顶点的入度是否均为0,如为0表示所有课程均可学完,若存在顶点值不为0,表示当前课程未能进行学习,返回false,否则返回true。

加餐练习题

加餐题和上文题思想不能说毫不相干,简直一模一样!

点击这里进行跳转 --> 课程表II

本篇文章看到这里已经渐渐步入尾声了,希望各位小伙伴看完本文后对拓扑排序会有一个新的认识,也要记得吃掉加餐嗷。以上就是本期分享的全部内容啦~~上文有不对的地方欢迎大家在评论区进行指正。

相关推荐
NAGNIP1 天前
轻松搞懂全连接神经网络结构!
人工智能·算法·面试
NAGNIP1 天前
一文搞懂激活函数!
算法·面试
董董灿是个攻城狮1 天前
AI 视觉连载7:传统 CV 之高斯滤波实战
算法
日月云棠1 天前
各版本JDK对比:JDK 25 特性详解
java
爱理财的程序媛1 天前
openclaw 盯盘实践
算法
用户8307196840821 天前
Spring Boot 项目中日期处理的最佳实践
java·spring boot
JavaGuide1 天前
Claude Opus 4.6 真的用不起了!我换成了国产 M2.5,实测真香!!
java·spring·ai·claude code
IT探险家1 天前
Java 基本数据类型:8 种原始类型 + 数组 + 6 个新手必踩的坑
java
花花无缺1 天前
搞懂new 关键字(构造函数)和 .builder() 模式(建造者模式)创建对象
java