LeetCode算法小抄 -- 环检测算法 和 拓扑排序算法

LeetCode算法小抄 -- 环检测算法 和 拓扑排序算法

      • 环检测算法(DFS)
        • [207. 课程表](https://leetcode.cn/problems/course-schedule/)
      • 拓扑排序算法(DFS)
        • [210. 课程表 II](https://leetcode.cn/problems/course-schedule-ii/)
      • 环检测算法(BFS)
      • 拓扑排序算法(BFS)

⚠申明: 未经许可,禁止以任何形式转载,若要引用,请标注链接地址。 全文共计6510字,阅读大概需要3分钟
🌈更多学习内容, 欢迎👏关注👀【文末】我的个人微信公众号:不懂开发的程序猿
个人网站:https://jerry-jy.co/

环检测算法(DFS)

207. 课程表

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

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

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

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

首先想到的就是把问题转化成「有向图」这种数据结构,只要图中存在环,那就说明存在循环依赖

class Solution {
    // 记录一次递归堆栈中的节点
    boolean[] onPath;
    // 记录遍历过的节点,防止走回头路
    boolean[] visited;
    // 记录图中是否有环
    boolean hasCycle = false;

    public boolean canFinish(int numCourses, int[][] prerequisites) {
        List<Integer>[] graph = buildGraph(numCourses, prerequisites);
        
        visited = new boolean[numCourses];
        onPath = new boolean[numCourses];
        
        for (int i = 0; i < numCourses; i++) {
            // 遍历图中的所有节点
            traverse(graph, i);
        }
        // 只要没有循环依赖可以完成所有课程
        return !hasCycle;
    }
	
    /** 判断图中是否存在环了  */
    private void traverse(List<Integer>[] graph, int s) {
        if (onPath[s]) {
            // 出现环
            hasCycle = true;
        }
        
        if (visited[s] || hasCycle) {
            // 如果已经找到了环,也不用再遍历了
            return;
        }
        // 前序代码位置
        visited[s] = true;
        onPath[s] = true;
        for (int t : graph[s]) {
            traverse(graph, t);
        }
        // 后序代码位置
        onPath[s] = false;
    }

    /** 建图函数 */
    private List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) {
        // 图中共有 numCourses 个节点
        List<Integer>[] graph = new LinkedList[numCourses];
        for (int i = 0; i < numCourses; i++) {
            graph[i] = new LinkedList<>();
        }
        for (int[] edge : prerequisites) {
            int from = edge[1], to = edge[0];
            // 添加一条从 from 指向 to 的有向边
            // 边的方向是「被依赖」关系,即修完课程 from 才能修课程 to
            graph[from].add(to);
        }
        return graph;
    }
}

拓扑排序算法(DFS)

拓扑排序(Topological Sorting)直观地说就是,让你把一幅图「拉平」,而且这个「拉平」的图里面,所有箭头方向都是一致的,比如上图所有箭头都是朝右的。

很显然,如果一幅有向图中存在环,是无法进行拓扑排序的,因为肯定做不到所有箭头方向一致;反过来,如果一幅图是「有向无环图」,那么一定可以进行拓扑排序。

在这里插入图片描述

其实也不难看出来,如果把课程抽象成节点,课程之间的依赖关系抽象成有向边,那么这幅图的拓扑排序结果就是上课顺序

如何进行拓扑排序?

将后序遍历的结果进行反转,就是拓扑排序的结果

210. 课程表 II

现在你总共有 numCourses 门课需要选,记为 0numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai必须 先选修 bi

  • 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1]

返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组

class Solution {
    // 记录后序遍历结果
    List<Integer> postorder = new ArrayList<>();
    // 记录是否存在环
    boolean hasCycle = false;
    boolean[] visited, onPath;

    // 主函数
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        List<Integer>[] graph = buildGraph(numCourses, prerequisites);
        visited = new boolean[numCourses];
        onPath = new boolean[numCourses];
        // 遍历图
        for (int i = 0; i < numCourses; i++) {
            traverse(graph, i);
        }
        // 有环图无法进行拓扑排序
        if (hasCycle) {
            return new int[]{};
        }
        // 逆后序遍历结果即为拓扑排序结果
        Collections.reverse(postorder);
        int[] res = new int[numCourses];
        for (int i = 0; i < numCourses; i++) {
            res[i] = postorder.get(i);
        }
        return res;
    }

    // 图遍历函数
    void traverse(List<Integer>[] graph, int s) {
        if (onPath[s]) {
            // 发现环
            hasCycle = true;
        }
        if (visited[s] || hasCycle) {
            return;
        }
        // 前序遍历位置
        onPath[s] = true;
        visited[s] = true;
        for (int t : graph[s]) {
            traverse(graph, t);
        }
        // 后序遍历位置
        postorder.add(s);
        onPath[s] = false;
    }

    // 建图函数
    List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) {
        // 图中共有 numCourses 个节点
        List<Integer>[] graph = new LinkedList[numCourses];
        for (int i = 0; i < numCourses; i++) {
            graph[i] = new LinkedList<>();
        }
        for (int[] edge : prerequisites) {
            int from = edge[1], to = edge[0];
            // 添加一条从 from 指向 to 的有向边
            // 边的方向是「被依赖」关系,即修完课程 from 才能修课程 to
            graph[from].add(to);
        }
        return graph;
    }        
}

说一下为什么要把后序遍历的结果再反转?网上看到的拓扑排序算法不用对后序遍历结果进行反转,这是为什么呢?

原因是他建图的时候对边的定义和我不同。我建的图中箭头方向是「被依赖」关系,比如节点 1 指向 2,含义是节点 1 被节点 2 依赖,即做完 1 才能去做 2,如果你反过来,把有向边定义为「依赖」关系,那么整幅图中边全部反转,就可以不对后序遍历结果反转。具体来说,就是把我的解法代码中 graph[from].add(to); 改成 graph[to].add(from); 就可以不反转了。

把二叉树理解成一幅有向图,边的方向是由父节点指向子节点

在这里插入图片描述

按照我们的定义,边的含义是「被依赖」关系,那么上图的拓扑排序应该首先是节点 1,然后是 2, 3,以此类推

但显然标准的后序遍历结果不满足拓扑排序,而如果把后序遍历结果反转,就是拓扑排序结果了:

在这里插入图片描述

环检测算法(BFS)

// 主函数
public boolean canFinish(int numCourses, int[][] prerequisites) {
    // 建图,有向边代表「被依赖」关系
    List<Integer>[] graph = buildGraph(numCourses, prerequisites);
    // 构建入度数组
    int[] indegree = new int[numCourses];
    for (int[] edge : prerequisites) {
        int from = edge[1], to = edge[0];
        // 节点 to 的入度加一
        indegree[to]++;
    }

    // 根据入度初始化队列中的节点
    Queue<Integer> q = new LinkedList<>();
    for (int i = 0; i < numCourses; i++) {
        if (indegree[i] == 0) {
            // 节点 i 没有入度,即没有依赖的节点
            // 可以作为拓扑排序的起点,加入队列
            q.offer(i);
        }
    }

    // 记录遍历的节点个数
    int count = 0;
    // 开始执行 BFS 循环
    while (!q.isEmpty()) {
        // 弹出节点 cur,并将它指向的节点的入度减一
        int cur = q.poll();
        count++;
        for (int next : graph[cur]) {
            indegree[next]--;
            if (indegree[next] == 0) {
                // 如果入度变为 0,说明 next 依赖的节点都已被遍历
                q.offer(next);
            }
        }
    }

    // 如果所有节点都被遍历过,说明不成环
    return count == numCourses;
}


/** 建图函数 */
private List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) {
    // 图中共有 numCourses 个节点
    List<Integer>[] graph = new LinkedList[numCourses];
    for (int i = 0; i < numCourses; i++) {
        graph[i] = new LinkedList<>();
    }
    for (int[] edge : prerequisites) {
        int from = edge[1], to = edge[0];
        // 添加一条从 from 指向 to 的有向边
        // 边的方向是「被依赖」关系,即修完课程 from 才能修课程 to
        graph[from].add(to);
    }
    return graph;
}

总结下这段 BFS 算法的思路:

1、构建邻接表,和之前一样,边的方向表示「被依赖」关系。

2、构建一个 indegree 数组记录每个节点的入度,即 indegree[i] 记录节点 i 的入度。

3、对 BFS 队列进行初始化,将入度为 0 的节点首先装入队列。

4、开始执行 BFS 循环,不断弹出队列中的节点,减少相邻节点的入度,并将入度变为 0 的节点加入队列

5、如果最终所有节点都被遍历过(count 等于节点数),则说明不存在环,反之则说明存在环

所有节点都被遍历过一遍,也就说明图中不存在环。

反过来说,如果按照上述逻辑执行 BFS 算法,存在节点没有被遍历,则说明成环。

比如下面这种情况,队列中最初只有一个入度为 0 的节点:

在这里插入图片描述

当弹出这个节点并减小相邻节点的入度之后队列为空,但并没有产生新的入度为 0 的节点加入队列,所以 BFS 算法终止

在这里插入图片描述

你看,如果存在节点没有被遍历,那么说明图中存在环

拓扑排序算法(BFS)

// 主函数
public int[] findOrder(int numCourses, int[][] prerequisites) {
    // 建图,和环检测算法相同
    List<Integer>[] graph = buildGraph(numCourses, prerequisites);
    // 计算入度,和环检测算法相同
    int[] indegree = new int[numCourses];
    for (int[] edge : prerequisites) {
        int from = edge[1], to = edge[0];
        indegree[to]++;
    }

    // 根据入度初始化队列中的节点,和环检测算法相同
    Queue<Integer> q = new LinkedList<>();
    for (int i = 0; i < numCourses; i++) {
        if (indegree[i] == 0) {
            q.offer(i);
        }
    }

    // 记录拓扑排序结果
    int[] res = new int[numCourses];
    // 记录遍历节点的顺序(索引)
    int count = 0;
    // 开始执行 BFS 算法
    while (!q.isEmpty()) {
        int cur = q.poll();
        // 弹出节点的顺序即为拓扑排序结果
        res[count] = cur;
        count++;
        for (int next : graph[cur]) {
            indegree[next]--;
            if (indegree[next] == 0) {
                q.offer(next);
            }
        }
    }

    if (count != numCourses) {
        // 存在环,拓扑排序不存在
        return new int[]{};
    }
    
    return res;
}

/** 建图函数 */
private List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) {
    // 图中共有 numCourses 个节点
    List<Integer>[] graph = new LinkedList[numCourses];
    for (int i = 0; i < numCourses; i++) {
        graph[i] = new LinkedList<>();
    }
    for (int[] edge : prerequisites) {
        int from = edge[1], to = edge[0];
        // 添加一条从 from 指向 to 的有向边
        // 边的方向是「被依赖」关系,即修完课程 from 才能修课程 to
        graph[from].add(to);
    }
    return graph;
}

–end–

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/12472.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python Web开发技巧II

Postman安置Cookie 对于大型项目而已&#xff0c;所携带的cookie往往都不止一个&#xff0c;而是一堆&#xff0c;甚至特别特别长&#xff0c;postman文档提供的cookie操作是全局的&#xff0c;但需要一个一个打&#xff08;折磨&#xff09;&#xff0c;唯一的优点就是作用域…

3.7 曲率

学习目标&#xff1a; 如果我要学习高等数学中的曲率&#xff0c;我会遵循以下步骤&#xff1a; 1.熟悉相关的数学概念&#xff1a;在学习曲率之前&#xff0c;我们需要了解曲线、切线和曲率半径等相关的数学概念。因此&#xff0c;我会复习这些概念&#xff0c;以便更好地理…

Java阶段一Day21

Java阶段一Day21 文章目录 Java阶段一Day21多线程并发原理使用场景创建并启动线程创建线程的方法 进程线程的生命周期获取线程信息的方法 教师总结新单词多线程概念线程:一个顺序的单一的程序执行流程就是一个线程。代码一句一句的有先后顺序的执行。多线程:多个单一顺序执行的…

Nacos 客户端服务注册源码分析-篇二

Nacos 客户端服务注册源码分析-篇二 继续接上回&#xff0c;上回分析到 NacosNamingService 的整个注册的流程&#xff0c;其实是通过 NacosFactory.createNamingService 方法&#xff0c;反射获取 NacosNamingService 接口的实现类 NacosNamingService &#xff0c;而 NacosN…

【计算方法】正交区域查询---KD-Tree概念

一、说明 kd 树是一种二叉树数据结构&#xff0c;可以用来进行高效的 kNN 计算。kd 树算法偏于复杂&#xff0c;本篇将先介绍以二叉树的形式来记录和索引空间的思路&#xff0c;以便读者更轻松地理解 kd 树。 二、正交区域查找 2.1 定义 对于k维空间的张量数据表格&#xff0…

一键生成元宇宙 AI又杀疯了

人类十几年的进步水平&#xff0c;AI用几个月就能轻易实现。在展示了超强的文本对话能力和一键生图功能后&#xff0c;AI大模型不打算停下&#xff0c;开始挑战搭建3D空间这一更高难度的动作。 这次&#xff0c;Facebook母公司Meta想当一把主导者。几天前&#xff0c;它的首席…

信号生成和可视化——周期性/非周期性波形

信号生成和可视化 此示例说明如何使用 Signal Processing Toolbox™ 中提供的函数生成广泛使用的周期和非周期性波形、扫频正弦波和脉冲序列。尝试此示例Copy Command Copy Code 周期性波形 除了 MATLAB 中的 sin 和 cos 函数外&#xff0c;Signal Processing Toolbox™ 还…

redis主从复制详解

文章目录 主从复制概述主从复制的作用主要包括&#xff1a;数据冗余故障恢复负载均衡高可用基石 主从库之间采用的是读写分离的方式读操作写操作 主从复制原理全量复制确立主从关系全量复制的三个阶段第一阶段是主从库间建立连接、协商同步的过程&#xff0c;主要是为全量复制做…

MobileNetV3详细原理(含torch源码)

作者&#xff1a;爱笑的男孩。 个人简介&#xff1a;打工人。 持续分享&#xff1a;机器学习、深度学习、python相关内容、日常BUG解决方法及Windows&Linux实践小技巧。 如发现文章有误&#xff0c;麻烦请指出&#xff0c;我会及时去纠正。有其他需要可以私信我或者发我邮箱…

Linux系统上如何禁用 USB 存储

Linux系统上如何禁用 USB 存储 为了保护数据不被泄漏&#xff0c;我们使用软件和硬件防火墙来限制外部未经授权的访问&#xff0c;但是数据泄露也可能发生在内部。 为了消除这种可能性&#xff0c;机构会限制和监测访问互联网&#xff0c;同时禁用 USB 存储设备。 我是艾西&…

uni-app常用配置

保存自动格式化 工具》设置》编辑器设置》保存时自动格式化 JS语法检查 安装eslint-js插件eslint-js - DCloud 插件市场用于校验js和html中的js代码https://ext.dcloud.net.cn/plugin?id2037工具》设置》插件配置》eslint-js 启用实时校检 Vue语法检查 安装eslint-vue插件…

如何突破LinkedIn领英限制,导出非好友邮箱等社交方式

相信做外贸的朋友都有使用过Linkedin&#xff0c;如果还没有使用过的话&#xff0c;我只能说您错过一个很好的平台。只要是厉害的外贸人都特别擅长用Linkedin找客户。 那为什么说Linkedin是外贸业务员开发客户最有效的途径呢&#xff1f;主要基于以下几点&#xff1a; 第一&a…

强训之【井字棋和密码强度等级】

目录 1.井字棋1.1题目1.2思路讲解1.3代码展示 2.密码强度判断2.1题目2.2思路讲解2.3代码 3.选择题 1.井字棋 1.1题目 链接: link 描述 给定一个二维数组board&#xff0c;代表棋盘&#xff0c;其中元素为1的代表是当前玩家的棋子&#xff0c;0表示没有棋子&#xff0c;-1代表…

禅道OpenAI更新至1.2版本,超多实用功能惊喜上线!

广受欢迎的禅道OpenAI插件近日成功发布&#xff0c;截至目前已更新至1.2版本。 截至本版本发布&#xff0c;禅道OpenAI已经拥有了神奇海螺&#xff08;ChatGPT聊天&#xff09;、需求润色、任务润色、Bug润色及本次的需求一键生成用例功能&#xff0c;仍有更多实用的新功能正在…

计算机视觉__基本图像操作(显示、读取、保存)

计算机视觉__基本图像操作&#xff08;显示、读取、保存&#xff09; 本文目录&#xff1a; ✨ 一、前言 ✨ 二、图像显示&#xff08;使用OpenCV和Matplotlib显示图像&#xff09; &#xff08;1&#xff09;、使用OpenCV显示图像 &#xff08;2&#xff09;、使用Matplotl…

电磁兼容(EMC)的标准与测试内容

在国际范围上&#xff0c;电磁兼容标准的制定已经有了70多年的发展历程&#xff0c;最早为了保护无线电通信和广播&#xff0c;国际无线电干扰特别委员会&#xff08;CISPR&#xff09;对各种用电设备和系统提出了相关的电磁干扰发射限值和测量方法。到了20世纪60&#xff5e;7…

被裁了,39 岁阿里 P9,攒下 1.5 亿....

今天刷知乎&#xff0c;在问题 “40 岁因为财务自由决定不上班的人&#xff0c;个人资产总和到底有多少” 下看到一位阿里 P9 的匿名回答让我狠狠的酸了一把~ 这刚刚失业的四十岁高级码农自曝了自己的人生经历&#xff0c;作为一名“阿里 P9”的程序员&#xff0c;他讲述了自己…

聚观早报|阿里云正式推出通义千问;京东零售开启5年最大组织变革

今日要闻&#xff1a;国家网信办规范生成式人工智能服务&#xff1b;阿里云正式推出通义千问&#xff1b;京东零售开启5年来最大组织变革&#xff1b;飞书将推出智能AI助手「My AI」&#xff1b;乐高将继续扩大在华零售布局 国家网信办规范生成式人工智能服务 4 月 11 日&…

redis——使用

session缓存缓存更新方式删除缓存vs更新缓存缓存和数据库操作原子性缓存和数据库操作顺序结论 缓存问题缓存穿透缓存雪崩缓存击穿 全局唯一ID数据并发线程安全单体分布式redis分布式锁的问题 redis消息队列listpubsubstream 消息推送 session 问题&#xff1a;session存在tomca…

nginx简单使用与配置

nginx简单使用与配置 Nginx 是一个高性能的HTTP和反向代理web服务器、一个邮件代理服务器&#xff0c;一个通用的 TCP/UDP 代理服务器。支持FastCGI、SSL、Virtual Host、URL Rewrite、Gzip等功能。并且支持很多第三方的模块扩展。 前端可以通过nginx实现以下功能&#xff1a…