【LeetCode热题100】打卡第38天:课程表实现前缀树

news2024/11/26 4:24:58

文章目录

  • 【LeetCode热题100】打卡第38天:课程表&实现前缀树
    • ⛅前言
  • 课程表
    • 🔒题目
    • 🔑题解
  • 实现前缀树
    • 🔒题目
    • 🔑题解

【LeetCode热题100】打卡第38天:课程表&实现前缀树

⛅前言

大家好,我是知识汲取者,欢迎来到我的LeetCode热题100刷题专栏!

精选 100 道力扣(LeetCode)上最热门的题目,适合初识算法与数据结构的新手和想要在短时间内高效提升的人,熟练掌握这 100 道题,你就已经具备了在代码世界通行的基本能力。在此专栏中,我们将会涵盖各种类型的算法题目,包括但不限于数组、链表、树、字典树、图、排序、搜索、动态规划等等,并会提供详细的解题思路以及Java代码实现。如果你也想刷题,不断提升自己,就请加入我们吧!QQ群号:827302436。我们共同监督打卡,一起学习,一起进步。

LeetCode热题100专栏🚀:LeetCode热题100

Gitee地址📁:知识汲取者 (aghp) - Gitee.com

题目来源📢:LeetCode 热题 100 - 学习计划 - 力扣(LeetCode)全球极客挚爱的技术成长平台

PS:作者水平有限,如有错误或描述不当的地方,恳请及时告诉作者,作者将不胜感激

课程表

🔒题目

原题链接:207.课程表

image-20230715111626221

🔑题解

  • 解法一:DFS(超时了,50个示例数据,过了42个,还有6个超时)

    感觉最容易想到的还剩带有邻接矩阵的DFS,但是这种空间占用比较大,然后利用一个 vis 数组对遍历的节点进行标记,遍历过的节点标记为true,未遍历的节点标记为 false,如果深度遍历的过程中遇到了已遍历的节点,则说明当前图存在环,思路比较简单

    image-20230717151054799

    image-20230717151227094

    /**
     * @author ghp
     * @title
     * @description
     */
    public class Solution {
        // 用于标记是否存在环
        private boolean hasCycle = false;
    
        public boolean canFinish(int numCourses, int[][] prerequisites) {
            boolean[][] vis = new boolean[numCourses][numCourses];
            // 初始化图
            int[][] graph = new int[numCourses][numCourses];
            for (int i = 0; i < prerequisites.length; i++) {
                int row = prerequisites[i][0];
                int col = prerequisites[i][1];
                graph[row][col] = 1;
            }
            // 判断图中是否有环
            for (int i = 0; i < numCourses; i++) {
                for (int j = 0; j < numCourses; j++) {
                    // 当前节点有下一个节点时,才开始DFS遍历
                    dfs(graph, vis, i, j);
                    if (hasCycle) {
                        // 如果已经发现了环,则直接返回
                        return false;
                    }
                }
            }
            return true;
        }
    
        private void dfs(int[][] graph, boolean[][] vis, int i, int j) {
            if (hasCycle) {
                // 已发现环,直接结束搜索
                return;
            }
            if (vis[i][j]) {
                // 当前节点已经走过了,说明有环,更新标记,结束搜索
                hasCycle = true;
                return;
            }
            for (int k = 0; k < graph.length; k++) {
                if (graph[i][j] == 1) {
                    // 当前节点有下一个节点,可以往下搜索
                    vis[i][j] = true;
                    dfs(graph, vis, j, k);
                    vis[i][j] = false;
                }
            }
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n 3 ) O(n^3) O(n3),遍历每一个节点时间复杂度是 n 2 n^2 n2,DFS搜索耗时是 n n n,两者是嵌套,所以这里的总的时间复杂度是 n 3 n^3 n3
    • 空间复杂度: O ( n 2 ) O(n^2) O(n2),graph占用空间 n 2 n^2 n2,vis占用空间 n 2 n^2 n2

    其中 n n n 为课程的数量

    代码优化:空间和时间优化(超时了,50个示例数据,过了46个,还有4个超时)

    • 空间优化:我们发现每次 vis 我们每次的DFS只需要用到一层的,所以我们可以将二维的vis压缩成一维的vis
    • 时间优化:前面我们是遍历了图的每一个节点,但是发现并没有这个必要,我们只需一层一层的遍历即可,DFS只需传当前层的节点,从而降低一个指数级的时间复杂度
    public class Solution {
        // 用于标记是否存在环
        private boolean hasCycle = false;
    
        public boolean canFinish(int numCourses, int[][] prerequisites) {
            // 初始化图
            int[][] graph = new int[numCourses][numCourses];
            for (int i = 0; i < prerequisites.length; i++) {
                int row = prerequisites[i][0];
                int col = prerequisites[i][1];
                graph[row][col] = 1;
            }
            // 判断图中是否有环
            for (int i = 0; i < numCourses; i++) {
                boolean[] vis = new boolean[numCourses];
                // DFS判断当前节点出发是否存在环
                dfs(graph, vis, i);
                if (hasCycle) {
                    // 如果已经发现了环,则直接返回
                    return false;
                }
            }
            return true;
        }
    
        private void dfs(int[][] graph, boolean[] vis, int i) {
            if (hasCycle) {
                // 已发现环,直接结束搜索
                return;
            }
            if (vis[i]) {
                // 当前节点已经走过了,说明有环,更新标记,结束搜索
                hasCycle = true;
                return;
            }
            for (int j = 0; j < graph[i].length; j++) {
                if (graph[i][j] == 1) {
                    // 当前节点有下一个节点,则遍历下一个节点
                    vis[i] = true;
                    dfs(graph, vis, j);
                    vis[i] = false;
                }
            }
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n 2 ) O(n^2) O(n2),遍历一层的节点时间复杂度是 n n n,DFS搜索时间复杂度是 n n n,两者嵌套,所以总的时间复杂度是 n 2 n^2 n2
    • 空间复杂度: O ( n 2 ) O(n^2) O(n2),vis占用空间 n n n,graph占用空间 n 2 n^2 n2

    其中 n n n 为课程的数量

    代码优化:时间和空间优化

    使用vis数组对每一个节点进行标记,用于整体判断节点是否已被遍历,使用path数组标记当前节点是否已被遍历,并且用于回溯。

    这样可以直接利用vis数组进行剪枝,只要遍历过的节点,可以直接不用重新遍历了

    不想画图了…………┭┮﹏┭┮大家自信参考代码理解,如果有任何不理解的地方,欢迎在评论区提问,随时在线解答

    注意:并不是说使用邻接表一定要比使用邻接矩阵的DFS的时间复杂度和空间复杂度要更加优秀,这个需要看具体情况,如果图的边比较多,优先推荐使用邻接矩阵,如果图的节点比较多,优先推荐使用邻接表。总的来讲稀疏图使用邻接表,稠密图使用邻接矩阵

    import java.util.List;
    
    /**
     * @author ghp
     * @title
     * @description
     */
    public class Solution {
        // 用于标记是否存在环
        private boolean hasCycle = false;
    
        public boolean canFinish(int numCourses, int[][] prerequisites) {
            // 构建邻接表
            List<List<Integer>> graph = buildGraph(numCourses, prerequisites);
            boolean[] vis = new boolean[numCourses];
            for (int i = 0; i < numCourses; i++) {
                if (!vis[i] && hasCycle) {
                    // 当前课程没有被遍历 并且 当前还没有发现环,则说明可以继续遍历
                    dfs(graph, i, vis, new boolean[numCourses]);
                }
            }
            return hasCycle;
        }
    
        private void dfs(List<List<Integer>> graph, int course, boolean[] vis, boolean[] path) {
            vis[course] = true;
            path[course] = true;
            for (int preCourse : graph.get(course)) {
                if (path[preCourse]) {
                    // 当前课的先行课已经被访问过了,说明出现了环
                    hasCycle = true;
                    return;
                }
                if (!vis[preCourse]) {
                    // 当前层并没有被访问过
                    dfs(graph, preCourse, vis, path);
                }
            }
            path[course] = false;
        }
    
        private List<List<Integer>> buildGraph(int numCourses, int[][] prerequisites) {
            List<List<Integer>> graph = new ArrayList<>();
            for (int i = 0; i < numCourses; i++) {
                graph.add(new ArrayList<>());
            }
            for (int[] prerequisite : prerequisites) {
                int course = prerequisite[0];
                int preCourse = prerequisite[1];
                graph.get(course).add(preCourse);
            }
            return graph;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n + m ) O(n+m) O(n+m)
    • 空间复杂度: O ( n + m ) O(n+m) O(n+m)

    其中 n n n 为课程的数量, m m m是课程的关系(说白了就是图的边)

    另一种写法,参考自这位大佬Krahets。感觉这种方法更加优雅😄

    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @author ghp
     * @title
     * @description
     */
    public class Solution {
        // 用于标记是否存在环
        private boolean hasCycle = false;
    
        public boolean canFinish(int numCourses, int[][] prerequisites) {
            // 构建邻接表
            List<List<Integer>> graph = buildGraph(numCourses, prerequisites);
            // 用于标记当前节点是否遍历 0:未遍历 1:已遍历发现环 -1:已遍历未发现环
            int[] vis = new int[numCourses];
            for (int i = 0; i < numCourses; i++) {
                if (vis[i] == 0 && !hasCycle) {
                    // 当前课程没有被遍历 并且 当前还没有发现环,则说明可以继续遍历
                    dfs(graph, i, vis);
                }
            }
            return !hasCycle;
        }
    
        private void dfs(List<List<Integer>> graph, int i, int[] vis) {
            if (vis[i] == 1) {
                // 已遍历发现环
                return;
            }
            if (vis[i] == -1) {
                // 已遍历未发现环
                return;
            }
            vis[i] = 1;
            for (int j : graph.get(i)) {
                if (vis[j] == 1) {
                    // 当前课程的先行课已被遍历,说明当前出现了环
                    hasCycle = true;
                    return;
                }
                // 当前先行课未被遍历,可以继续放下遍历
                dfs(graph, j, vis);
            }
            vis[i] = -1;
        }
    
        private List<List<Integer>> buildGraph(int numCourses, int[][] prerequisites) {
            List<List<Integer>> graph = new ArrayList<>();
            for (int i = 0; i < numCourses; i++) {
                graph.add(new ArrayList<>());
            }
            for (int[] prerequisite : prerequisites) {
                int course = prerequisite[0];
                int preCourse = prerequisite[1];
                graph.get(course).add(preCourse);
            }
            return graph;
        }
    }
    
  • 解法二:BFS(拓扑排序)

    这里思想很巧妙,由于我之前没有接触过拓扑排序,所以一开始看到这个题解是十分懵逼的(心想我是谁?我在哪?我要干啥?)

    在补充了拓扑排序相关知识后,再来看这题解,发现并没有之前那么懵逼了,反而觉得比前面的DFS还要简单O(∩_∩)O

    首先我们来了解一下什么是拓扑排序:拓扑排序(Topological sort)是对有向无环图(DAG,Directed Acyclic Graph)进行排序的一种算法。在拓扑排序中,图中的节点表示任务或事件,有向边表示任务间的依赖关系

    一上来看到这个定义我是一脸懵逼的,这里我就画一个图来快速理解一下拓扑排序吧(一图胜千言):

    无环的情况,能够遍历所有的节点:

    image-20230717165458177

    有环的情况,不能够遍历完所有的节点:

    image-20230717165958715

    拓扑排序的目标是将图中的节点按照依赖关系进行排序,即满足所有的先决条件。如果存在依赖关系循环(即存在环),则无法进行拓扑排

    同样的这个题解也是参考 K神 的(●ˇ∀ˇ●)

    import java.util.ArrayList;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.Queue;
    
    /**
     * @author ghp
     * @title
     * @description
     */
    public class Solution {
        public boolean canFinish(int numCourses, int[][] prerequisites) {
            // 记录节点的入读
            int[] indegree = new int[numCourses];
            // 构建邻接表
            List<List<Integer>> graph = buildGraph(numCourses, prerequisites, indegree);
            // 初始化队列,将所有入度为0的节点加入队列
            Queue<Integer> queue = new LinkedList<>();
            for (int i = 0; i < numCourses; i++) {
                if (indegree[i] == 0){
                    queue.add(i);
                }
            }
            // BFS遍历所有节点
            while (!queue.isEmpty()){
                int pre = queue.poll();
                // 移除前一个节点
                numCourses--;
                for (Integer cur : graph.get(pre)) {
                    if (--indegree[cur] == 0){
                        // 当前节点前一个节点被移除后,当前节点的入读变为了0,则入读
                        queue.add(cur);
                    }
                }
            }
            // 判断是否遍历完了所有节点,如果遍历完了所有节点则说明不存在环,反之则说明存在环
            return numCourses == 0;
        }
    
        private List<List<Integer>> buildGraph(int numCourses, int[][] prerequisites, int[] indegree) {
            List<List<Integer>> graph = new ArrayList<>();
            for (int i = 0; i < numCourses; i++) {
                graph.add(new ArrayList<>());
            }
            for (int[] prerequisite : prerequisites) {
                int course = prerequisite[0];
                int preCourse = prerequisite[1];
                graph.get(course).add(preCourse);
                indegree[preCourse]++;
            }
            return graph;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( ) O() O()
    • 空间复杂度: O ( ) O() O()

    其中 n n n 为数组中元素的个数

实现前缀树

🔒题目

原题链接:208.实现前缀树

image-20230717193500460

🔑题解

  • 解法一:前缀树

    首先我们来了解一下什么是前缀树:前缀树(Trie)是一种多叉树的数据结构,用于高效地存储和检索字符串集合。它也被称为字典树、单词查找树或键树。前缀树的特点是每个节点代表一个字符,从根节点到叶子节点的路径表示一个完整的字符串。通过不同的路径可以区分不同的字符串。根节点不包含字符,每个非根节点都有一个与之对应的字符值。在前缀树中,具有相同前缀的字符串会共享相同的前缀路径。

    说白了这个前缀树就是一颗26叉树,每一个节点下都有26个子节点

    /**
     * @author ghp
     * @title
     * @description
     */
    public class Solution {
    }
    
    class Trie {
        // 是否存在以某一个单词结尾的树
        private boolean isExit;
        // 节点的分支
        private Trie[] children;
    
        public Trie() {
            this.isExit = false;
            this.children = new Trie[26];
        }
    
        /**
         * 新增单词
         * @param word
         */
        public void insert(String word) {
            Trie root = this;
            // 构建前缀树,将字符串中的字符
            for (int i = 0; i < word.length(); i++) {
                char ch = word.charAt(i);
                int index = ch - 'a';
                if (root.children[index] == null) {
                    // 后面为null,
                    root.children[index] = new Trie();
                }
                // 将指针移动到子节点,构建下一层节点
                root = root.children[index];
            }
            // 将当前字符的最后一个单词结的isExit属性标记为true
            root.isExit = true;
        }
    
        /**
         * 查询单词
         * @param word
         * @return
         */
        public boolean search(String word) {
            Trie root = searchPrefix(word);
            // 当前节点存在,并且是以当前单词最后一个字母结尾的
            return root != null && root.isExit;
        }
    
        /**
         * 查询前缀
         * @param prefix
         * @return
         */
        public boolean startsWith(String prefix) {
            return searchPrefix(prefix) != null;
        }
    
        private Trie searchPrefix(String prefix) {
            Trie root = this;
            for (int i = 0; i < prefix.length(); i++) {
                char ch = prefix.charAt(i);
                int index = ch - 'a';
                if (root.children[index] == null) {
                    // 当前字符不存在,直接返回null
                    return null;
                }
                // 遍历下一层节点
                root = root.children[index];
            }
            return root;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
    • 空间复杂度: O ( 1 ) O(1) O(1)

    其中 n n n 为数组中元素的个数

参考资料

  • 【图解算法】模板+变式——带你彻底搞懂字典树(Trie树)_

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

udx实现揭秘之---------udx的慢启动带宽探测

我们都知道bew wnd*1000/rtt 当慢启动的时候&#xff0c;每收一个ack,可以这样调整发送速度当前sendspeed sendspeed mss/rtt.并且更新wnd->wnd1. udx的变形版本是sendspeed checksize/rtt; 但是这种增加速度太快&#xff0c;在到达临界点的时候&#xff0c;很容易击…

低功耗设计:为可持续发展带来能源效率的突破

引言&#xff1a; 低功耗设计是面向电子设备和系统的一种设计理念&#xff0c;旨在降低设备的能耗和功耗&#xff0c;提高能源利用效率。在当前节能环保的背景下&#xff0c;低功耗设计成为了电子行业的热门话题&#xff0c;它对于延长电池寿命、提高设备性能和减少能源消耗具有…

选读SQL经典实例笔记08_区间查询

1. 计算同一组或分区的行之间的差 1.1. 最终结果集 1.1.1. sql DEPTNO ENAME SAL HIREDATE DIFF ------ ---------- ---------- ----------- ----------10 CLARK 2450 09-JUN-1981 -255010 KING 5000 17-NOV-1981 370010 …

20 - 队列 - 链队列——队列的链式表示和实现

前面我们学习了 先进先出、后进后出 的线性表- 队列,并且我们是使用数组进行了实现,那么这节课我们来使用链表来实现队列,即链队列! 队列的定义 队列(Queue)也是一种线性表, 但是它仅能在一端进行插入,而另一端进行删除的操作 ,插入的一端称为 队尾rear,删除的一端…

针对Weblogic上的shiro漏洞利用工具

工具介绍 日常项目中&#xff0c;可能会碰见部署在weblogic上的shiro&#xff0c;所以先写了这个生成攻击payload的小Demo&#xff0c;方便后面使用。但目前只支持无回显命令执行&#xff0c;后续回显、内存马功能后面出差后再写。 关注【Hack分享吧】公众号&#xff0c;回复关…

【HCIA】12.网络服务与应用

文件传输协议FTP FTP基于TCP&#xff0c;传输较慢&#xff0c;但是比较可靠。典型的C/S架构。双通道协议。TFTP基于UDP&#xff0c;传输较快&#xff0c;但是不可靠。FTP有两种不同的传输模式 ASCII模式 传输文本文件时会对文本内容进行编码方式转换&#xff0c;提高传输效率&…

音视频技术开发周刊 | 302

每周一期&#xff0c;纵览音视频技术领域的干货。 新闻投稿&#xff1a;contributelivevideostack.com。 ChatGPT神器Code Interpreter终于开放&#xff0c;到底怎么用&#xff1f;这里有一份保姆级教程 Code Interpreter 已经正式开放。 上海世界AI大会&#xff1a;MidJourney…

Fedproto:原型聚合的联邦学习框架

题目&#xff1a;FedProto: Federated Prototype Learning across Heterogeneous Clients 网址&#xff1a;http://arxiv.org/abs/2105.00243 目录 前言 什么是原型&#xff1f; Fedproto框架 fedproto settings Fedproto算法 优化目标 全局聚合 局部更新 伪代码 前言…

抖音seo源码搭建,抖音矩阵系统源码分发,抖音矩阵账号管理

前言&#xff1a; 抖音seo源码&#xff0c;抖音矩阵系统源码搭建&#xff0c;抖音矩阵同步分发。抖音seo源码部署是需要对接到这些正规接口再来做开发的&#xff0c;目前账号矩阵程序开发的功能&#xff0c;围绕一键管理多个账号&#xff0c;做到定时投放&#xff0c;关键词自动…

[Linux笔记]vim基础

vim本身不是像vs那样什么都能做的ide&#xff0c;只是单纯的编辑器。 命令行输入vim 文件名&#xff0c;会以vim打开文件。 若当前路径下尚无该名称的文件&#xff0c;则会在保存退出时创建该文件。 在vim中操作&#xff0c;尽量不要用鼠标及其滚轮操作&#xff0c;而只用键盘…

tql!AD域渗透信息收集可视化工具

工具介绍 adalanche是一款ad域中的ACL可视化及利用工具&#xff0c;和BloodHound功能类似&#xff0c;能方便域渗透人员快速发现域中的弱点&#xff1b;有开源版和商业两个版本。 关注【Hack分享吧】公众号&#xff0c;回复关键字【230709】获取下载链接 相比于BloodHound工具…

nest.js 添加 swagger 响应数据文档

基本使用 通常情况下&#xff0c;在 nest.js 的 swagger 页面文档中的响应数据文档默认如下 此时要为这个控制器添加响应数据文档的话&#xff0c;只需要先声明 数据的类型&#xff0c;然后通过ApiResponse 装饰器添加到该控制器上即可&#xff0c;举例说明 todo.entity.ts …

DP1044 CAN FD 待机模式总线收发器替代TJA1044

5V 供电&#xff0c;IO 口兼容 3.3V&#xff0c;5Mbps&#xff0c;CAN FD 待机模式总线收发器DP1044是一款应用于 CAN 协议控制器和物理总线之间的接口芯片&#xff0c;可应用于卡车、公交、小汽车、工业控制等领域&#xff0c;支持 5Mbps 灵活数据速率&#xff08;Flexible Da…

新零售商城系统开发流程,新零售商城系统的前景如何?

近10年来&#xff0c;新零售商城系统火爆的原因在于移动互联网普及、个性化需求的增加、优化用户体验、数据驱动的营销和线上线下融合。新零售商城系统是基于互联网技术的商城平台&#xff0c;通过线上线下融合、数据分析和个性化推荐等功能&#xff0c;为零售商提供全方位的销…

N!Weblogic CVE-2023-21839 RCE

项目简介 Weblogic CVE-2023-21839/CVE-2023-21931/CVE-2023-21979 一键检测工具&#xff0c;这是来自长亭xray的代码&#xff0c;该漏洞扫描已集成到新版本xray中。 关注【Hack分享吧】公众号&#xff0c;回复关键字【230708】获取下载链接 无需任何Java依赖&#xff0c;构造…

【C语言】unsigned 与 signed 详解

1. 基本概念 整数在存储单元中都是以补码形式存储的&#xff0c;存储单元中的第 1 个二进制位代表符号。整型变量的值的范围包括负数到正数。 但是在实际应用中&#xff0c;有的数据的范围常常只有正值&#xff08;如学号、年龄等&#xff09;&#xff0c;为了充分利用变量的值…

网盘工具alist在Windows中使用教程

alist 软件同时支持 http 协议和 WebDAV 协议&#xff0c;并且支持很多网盘种类&#xff0c;这样就给我们留下了很多可玩的空间&#xff0c;比如&#xff1a; 实现网盘本地化访问关联本地的播放器&#xff0c;以实现很好的播放效果多端文件互传&#xff0c;比如将阿里云盘的文…

GO语言slice

slice: data lencap 以及存取的元素是可以安全读写的 Slice 扩容。 1&#xff0c;预估&#xff1a; 2&#xff0c;预估容量后*字节数 所需的内存 3&#xff0c;各种语言从OS上提前申请内存&#xff0c;匹配GO规则的内存

nosql——Redis,Mongodb

目录 一、redis 1、 string类型数据的命令操作 2、 list类型数据的命令操作 3、 hash类型数据的命令操作 4、Keys相关的命令操作 二、mongodb 1. 创建一个数据库 名字grade 2. 数据库中创建一个集合名字 class 3. 集合中插入若干数据 文档格式如下 4. 查找 5. 增加、…

配置jenkins 服务器与目标服务器自动化部署

在配置完远程构建后可以通过添加post-build step 执行shell脚本的方式将包传到远程服务器等一系列操作。 通过scp传输打包好的项目到目标服务器 按照链接 方式配置免密操作&#xff0c;需要注意的是要在jenkins 用户目录下配置生成私钥密钥&#xff0c;配置jenkins 的免密&…