【LeetCode热题100】打卡第33天:环形链表LRU缓存

文章目录

  • 【LeetCode热题100】打卡第33天:环形链表&LRU缓存
    • ⛅前言
  • 环形链表
    • 🔒题目
    • 🔑题解
  • LRU缓存
    • 🔒题目
    • 🔑题解

【LeetCode热题100】打卡第33天:环形链表&LRU缓存

⛅前言

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

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

博客主页💖:知识汲取者的博客

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

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

Github地址📁:Chinafrfq · GitHub

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

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

环形链表

🔒题目

原题链接:142.环形链表II

image-20230705143314511

🔑题解

  • 解法一:Set集合

    昨天刚写完【LeetCode热题100】打卡第32天的题目,其中就遇到 环形链表I,也是使用这种方式解决的O(∩_∩)O

    public class Solution {
        public ListNode detectCycle(ListNode head) {
            Set<ListNode> set = new HashSet<>();
            while (head != null) {
                if (!set.add(head)) {
                    return head;
                }
                head = head.next;
            }
            return null;
        }
    }
    

    复杂度分析:

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

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

  • 解法二:快慢指针

    这个快慢指针用起来就要比【LeetCode热题100】打卡第32天的题目 的那道环形链表I 要难的多了

    详解参考 K神,真的是强,佩服b( ̄▽ ̄)d,这里我给出一些我的理解

    假设head到环入口出要走a步,环的节点数为b,则:

    1. fast于slow相遇,fast一定是比slow多走nb

      s , f = 2 s = s + n b → s = n b s,f=2s=s+nb → s=nb s,f=2s=s+nbs=nb

    2. a+nb一定是在环入口出

    3. 第一次相遇后,我们将fast重置到head处,这样就能保障fast和slow相遇一定是是a+nb,此时两者在环入口相遇

      f = 0 , s = n b → f = a , s = a + n b f=0,s=nb→f=a,s=a+nb f=0,s=nbf=a,s=a+nb

    这里面具有很严密的数据逻辑推理在里面!

    public class Solution {
        public ListNode detectCycle(ListNode head) {
            ListNode fast = head;
            ListNode slow = head;
            while (fast != null){
                slow = slow.next;
                fast = fast.next;
                if (fast!=null){
                    fast = fast.next;
                }
                if (fast == slow){
                    break;
                }
            }
            if (fast==null){
                return null;
            }
            fast = head;
            while (fast!=slow){
                slow = slow.next;
                fast = fast.next;
            }
            return fast;
        }
    }
    

    复杂度分析:

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

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

LRU缓存

🔒题目

原题链接:146.LRU缓存

image-20230705143338684

🔑题解

  • 解法一:Map标记法(超时,22个示例数据过了20个)

    这是最开始的思路,直接使用双Map,一个Map作为缓存,一个Map用于记录key的淘汰优先级,每次进行get或put操作时,未操作的key的淘汰优先级都自增1,如果缓存已满,则根据淘汰优先级进行淘汰。总的来说这个思路还是挺简单的,但是这代码看着就像“屎山代码”w(゚Д゚)w,感觉可以进行优化

    image-20230709160548375

    image-20230709160805622

    class LRUCache {
    
        // 缓存
        private Map<Integer, Integer> cache;
        // 用于标记,值越大越优先淘汰
        private Map<Integer, Integer> flag;
        // 最大容量
        private int MAX_CAPACITY;
    
    
        public LRUCache(int capacity) {
            MAX_CAPACITY = capacity;
            cache =  new HashMap<>(capacity);
            flag = new HashMap<>(capacity);
        }
    
        /**
         * 从缓存中获取值
         */
        public int get(int key) {
            if (cache.containsKey(key)){
                // 当前元素置0,其它元素值+1
                flag.put(key, 0);
                increment(key);
                return cache.get(key);
            }
            return -1;
        }
    
        /**
         * 除key以外的都自增
         */
        private void increment(int key) {
            for (Integer i : flag.keySet()) {
                if (i != key){
                    flag.put(i, flag.get(i)+1);
                }
            }
        }
    
        /**
         * 往缓存中添加元素
         */
        public void put(int key, int value) {
            if (cache.size() < MAX_CAPACITY){
                // 缓存容量足够,直接添加,并将新加入元素标记值置为初值0
                cache.put(key, value);
                flag.put(key, 0);
                increment(key);
                return;
            }
            if (cache.containsKey(key)){
                // 缓存容量不够,但是当前添加的key已在缓存中存在,直接更新即可
                cache.put(key, value);
                flag.put(key, 0);
                increment(key);
                return;
            }
            // 缓存容量不够且key不在缓存中,使用 LRU 策略淘汰缓存中的数据
            int i = getDieOutKey();
            cache.remove(i);
            cache.put(key, value);
            flag.put(key, 0);
            increment(key);
        }
    
        /**
         * 获取淘汰元素的索引
         */
        private int getDieOutKey() {
            int max = Integer.MIN_VALUE;
            int key = 0;
            for (Integer i : flag.keySet()) {
                if (flag.get(i)>max){
                    max = flag.get(i);
                    key = i;
                }
            }
            flag.remove(key);
            return key;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n ) O(n) O(n),每次put和get都需要调用increment方法,increment方法需要遍历整个map,getDieOutKey方法也需要遍历整个map,时间复杂度也是 O ( n ) O(n) O(n),但两者没有嵌套,所以总的时间复杂度是 O ( n ) O(n) O(n)
    • 空间复杂度: O ( n ) O(n) O(n)

    其中 n n n 为缓存的大小

    代码优化:使用队列代替Map标记(时间优化)

    上面我们是利用Map集合对存入缓存中的元素进行一个标记,每次往缓存中存入和获取,都需要遍历一遍 flag ,并且删除时也需要遍历一遍 flag,这就导致虽然看着时间复杂度是 ( n ) (n) (n),但是对于频繁的操作耗时是非常多的。

    上面的map标记法,我们可以知道最大耗时在于定位 flag 中最大的value,为了解决定位问题,我们可以采用队列,而不是map,队列具有先进先出的特点(队尾进,对头出),这就意味着我们可以将最旧的元素放到对头,最新的元素放到队尾。

    image-20230709162343302

    image-20230709162837765

    class LRUCache {
    
        // 缓存
        private Map<Integer, Integer> map;
        // 用于LRU淘汰
        private Queue<Integer> queue;
        // 最大容量
        private int MAX_CAPACITY;
    
        public LRUCache(int capacity) {
            MAX_CAPACITY = capacity;
            map = new HashMap<>(capacity);
            queue = new LinkedList<>();
        }
    
        public int get(int key) {
            if (map.containsKey(key)){
                queue.remove(key);
                queue.offer(key);
                return map.get(key);
            }
            return -1;
        }
    
        public void put(int key, int value) {
            if (map.containsKey(key)){
                // 缓存中存在该key,直接更新
                queue.remove(key);
                queue.offer(key);
                map.put(key, value);
                return;
            }
            if (map.size() < MAX_CAPACITY){
                // 缓存不存在该key,但当前缓存容量足够,直接添加
                queue.offer(key);
                map.put(key, value);
                return;
            }
            // 缓存容量不足,移除最先进入队列的元素
            int first = queue.poll();
            queue.add(key);
            map.remove(first);
            map.put(key, value);
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n ) O(n) O(n)queue.remove()方法需要遍历链表,时间复杂度是 O ( n ) O(n) O(n)
    • 空间复杂度: O ( n ) O(n) O(n)

    n为缓存中最大能存储元素的个数

    PS:显然这段代码比上上面那段代码就要好的多了,但是提交只能够击败5%的 Java选手,这说明还有更好的方法

  • 解法二:利用LinkedHashMap

    LinkedHashMap底层是使用一个 Map+双向链表,LinkedHashMap有一个最大容量

    class LRUCache extends LinkedHashMap<Integer, Integer>{
        // 最大容量
        private int capacity;
        
        public LRUCache(int capacity) {
            // 调用构造方法,第三个参数设置为true时,当LinkedHashMap达到最大容量时
            // 底层回采用LRU策略,移除最旧的元素
            super(capacity, 0.75F, true);
            this.capacity = capacity;
        }
    
        public int get(int key) {
            return super.getOrDefault(key, -1);
        }
    
        public void put(int key, int value) {
            super.put(key, value);
        }
    
        /**
        * 设置淘汰时机,当超过最大容量时按照LRU策略淘汰最旧的值
        */
        @Override
        protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
            return size() > capacity; 
        }
    }
    

    复杂度分析:

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

    其中 n n n 为缓存中元素的最大个数

    参照LinkedHashMap源码手写一个简易版的LinkedHashMap

    前面我们使用队列进行移除操作,时间复杂度是 O ( n ) O(n) O(n),因为队列底层是采用了单链表,单链表删除中间节点需要先遍历链表定位到要删除的节点的前驱节点,而现在我们使用一个双链表数据结构,我们直接可以通过 前驱指针pre 定位到要删除的节点前驱节点,进行删除操作,这就大大提高了删除的效率,从而提高了时间,但是提高了额外的内存开销(典型的空间换时间)

    image-20230709164346670

    class LRUCache {
    
        /**
         * 定义一个双链表
         */
        private class DLinkedList {
            int key;
            int value;
            // 前驱指针,用于维护当前节点与前驱节点的关系
            DLinkedList pre;
            // 后继指针,用于维护当前节点与后继节点的关系
            DLinkedList next;
            public DLinkedList() {}
            public DLinkedList(int key, int value) {
                this.key = key;
                this.value = value;
            }
        }
    
        /**
         * 缓存最大容量
         */
        private int capacity;
        /**
         * 缓存中的元素的个数(空间换时间)
         */
        private int size;
        /**
         * 双链表的头节点指针
         */
        DLinkedList head;
        /**
         * 双链表的尾节点指针
         */
        DLinkedList tail;
        /**
         * 缓存
         */
        private Map<Integer, DLinkedList> cache = new HashMap<>();
    
    
        public LRUCache(int capacity) {
            this.capacity = capacity;
            this.size = 0;
            this.head = new DLinkedList();
            this.tail = new DLinkedList();
            this.head.next = this.tail;
            this.tail.pre = this.head;
        }
    
        /**
         * 从缓存中取值
         */
        public int get(int key) {
            DLinkedList node = cache.get(key);
            if (node == null) {
                // 缓存未命中,直接返回-1
                return -1;
            }
            // 缓存命中,则更新双链表(将命中节点更新为双链表的头节点)
            moveToHead(node);
            return node.value;
        }
    
        /**
         * 往缓存中存值
         */
        public void put(int key, int value) {
            DLinkedList node = cache.get(key);
            if (node != null) {
                // 缓存命中,则更新双链表并直接返回命中的值
                node.value = value;
                moveToHead(node);
                return;
            }
            // 缓存未命中,需要判断当前缓存的容量是否充足
            if (size == capacity) {
                // 缓存容量已满,需要采用LRU策略移除最旧的值(也就是双链表的尾节点)
                DLinkedList tailNode = remove(tail.pre);
                cache.remove(tailNode.key);
                size--;
            }
            // 将新增的节点添加到链表头部,并存入缓存
            DLinkedList newNode = new DLinkedList(key, value);
            add(newNode);
            cache.put(key, newNode);
            size++;
        }
    
        /**
         * 将节点更新为双链表的头节点
         */
        public void moveToHead(DLinkedList node) {
            // 先移除,后添加,即可将节点更新为头节点
            remove(node);
            add(node);
        }
    
        /**
         * 移除节点(并返回被移除的节点)
         */
        private DLinkedList remove(DLinkedList node) {
            if (node.next == tail) {
                // 要移除的节点是尾节点
                node.pre.next = tail;
                tail.pre = node.pre;
            } else {
                // 要移除的节点是中间节点
                node.pre.next = node.next;
                node.next.pre = node.pre;
            }
            return node;
        }
    
        /**
         * 添加节点(从双链表的头部添加)
         */
        private void add(DLinkedList node) {
            node.pre = head;
            node.next = head.next;
            head.next.pre = node;
            head.next = node;
        }
    
    }
    

    复杂度分析:

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

    其中 n n n 为缓存中元素的最大个数

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

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

相关文章

Chapter 3: Conditional | Python for Everybody 讲义笔记_En

文章目录 Python for Everybody课程简介Chapter 3: Conditional executionBoolean expressionsLogical operatorsConditional executionAlternative executionChained conditionalsNested conditionalsCatching exceptions using try and exceptShort-circuit evaluation of lo…

运维开发面试题第一期

1.tail -f和tail -F的区别是什么? tail -f 根据文件描述符进行追踪&#xff0c;当文件改名或被删除&#xff0c;追踪停止。 tail -F 根据文件名进行追踪&#xff0c;并保持重试&#xff0c;即该文件被删除或改名后&#xff0c;如果再次创建相同的文件名&#xff0c;会继续…

Tomcat相关

1. 运行项目 将java项目打包为war或者war所对应的文件夹&#xff0c;放置于tomcat的webapps目录下。其实tomcat运行时会解压war到项目中并运行class文件&#xff0c;延伸开来&#xff0c;为啥不能用jar包&#xff0c;因为jar可能可以表示项目但也能表示依赖&#xff0c;tomcat…

国产4 通道模拟复合视频解码芯片MIPI CSI 接口,XS9922B

XS9922B 是一款 4 通道模拟复合视频解码芯片&#xff0c;支持 HDCCTV 高清协议和 CVBS 标 清协议&#xff0c;视频制式支持 720P/1080P 高清制式和 960H/D1 标清制式。芯片将接收到的高清 模拟复合视频信号经过模数转化&#xff0c;视频解码以及 2D 图像处理之后…

git在工作中如何搭建和运用(巨详细!!)

最近有点闲&#xff0c;出一版git在实际公司上的一些运用 1&#xff0c;下载git&#xff0c; 下载git就不多说了&#xff0c;官方上下载安装就好了。 2&#xff0c;初始化 下载安装完成后&#xff0c;找个项目的空文件夹进去&#xff0c;右键点击git bash here &#xff0c;…

Android 视频直播提拉流 嵌入式硬件 流媒体开发详细内容

1 Linux 系统编程网络编程基础 2 Linux 网络编程流媒体服务器&#xff0c;客户端开发实践 3 Android流媒体客户端 FFmpeg OpenGL ES 开发实践 4 Android H.264 AAC 封装mp4开发实战 5 流媒体开发实战之Rtmp推流 6 流媒体开发实战之RTSP推流 7 流媒体开发实战之UDP 8 P2P点对点项…

培训报名小程序报名列表页开发

目录 1 创建页面2 组件搭建3 设置URL参数4 设置筛选条件5 首页跳转6 最终的效果总结 这节我们来开发报名列表功能&#xff0c;先看原型 1 创建页面 功能要在页面上呈现&#xff0c;需要先创建页面。打开我们的培训报名小程序&#xff0c;在页面区&#xff0c;点击创建页面的…

多元回归预测 | Matlab主成分分析PCA降维,PLS偏小二乘回归预测。PCA-PLS回归预测模型

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 多元回归预测 | Matlab主成分分析PCA降维,PLS偏小二乘回归预测。PCA-PLS回归预测模型 评价指标包括:MAE、RMSE和R2等,代码质量极高,方便学习和替换数据。要求2018版本及以上。 部分源码 %% 清空环境变量 warn…

CUDA+CUDNN+torch+torchvision安装

弄了好久&#xff0c;终于弄好了&#xff01;&#xff01;&#xff01; 原因&#xff1a;其实之前我是已经配置好pytorch的相关环境的了。但是这段时间&#xff0c;在跑GNN相关论文中的代码时&#xff0c;发现代码中的某个函数要求torch必须得是1.8 而我之前安装的是torch1.1…

《MySQL技术内幕》读书总结(一):MySQL体系结构和存储引擎

文章目录 前言&#xff1a;1、定义数据库和实例2、MySQL体系结构3、MySQL存储引擎InnoDBMyISAM 4、连接MySQL 前言&#xff1a; 该技术文章是我阅读《MySQL技术内幕 InnoDB存储引擎》第2版的总结梳理 我写这里文章的目的&#xff1a;书中的内容过于系统和繁琐&#xff0c;并不是…

C++学习 数组

目录 数组 一维数组 数组名 案例&#xff1a;冒泡排序 二维数组 数组名 数组 数组就是一个集合&#xff0c;里面存放了相同类型的数据元素。 下面的数字对应为数组的下标(索引)&#xff0c;可以看到索引范围为0~数组长度-1 特点&#xff1a; 数组中数据元素的数据类型相同。…

Unity3D 场景添加obj模型

有一个立方体的obj模型&#xff1b;将其拖到Assets文件夹节点上&#xff0c;在此节点放手&#xff0c;资源被加入项目&#xff1b; 在右侧显示出对象概览&#xff1b; 点击箭头&#xff0c;显示此模型下的子对象&#xff1b; 然后按住Assets面板中的cube1对象&#xff0c;拖动…

36.RocketMQ之Broker如何实现磁盘文件高性能读写

highlight: arduino-light Broker读写磁盘文件的核心技术:mmap Broker中大量的使用mmap技术去实现CommitLog这种大磁盘文件的高性能读写优化的。 通过之前的学习&#xff0c;我们知道了一点&#xff0c;就是Broker对磁盘文件的写入主要是借助直接写入os cache来实现性能优化的&…

【Java项目】Vue+ElementUI+Ceph实现多类型文件上传功能并实现文件预览功能

文章目录 效果演示前端后端Java 效果演示 先说一下我们的需求&#xff0c;我们的需求就是文件上传&#xff0c;之前的接口是只支持上传图片的&#xff0c;之后需求是需要支持上传pdf&#xff0c;所以我就得换接口&#xff0c;把原先图片上传的接口换为后端ceph&#xff0c;但是…

诚迈科技董事长、统信软件董事长王继平出席全球数字经济大会

7月5日&#xff0c;2023全球数字经济大会“数字未来新一代软件产业高质量发展论坛”在北京大兴隆重举行。论坛以“数字新高地&#xff0c;数创兴未来”为主题&#xff0c;共同探讨产业升级新路径&#xff0c;凝聚数字经济合作新共识&#xff0c;构建数字产业集聚发展新高地。诚…

基于Qt5 实现的简易慕课爬取程序

基于Qt5 实现的简易慕课爬取程序 一、项目概述二、源代码 一、项目概述 名称&#xff1a;MookScrapy 这个项目主要是使用了 Qt 里面的 QNetworkAccessManager 去下载慕课网站的数据 https://coding.imooc.com&#xff0c;也就是这个网站里面的卡片信息。然后做一定的分析和展示…

每次装完 homebrew,ohmyzsh 就会报错:Insecure completion-dependent directories detected:

参考:https://zhuanlan.zhihu.com/p/313037188 这是因为在big sur安装homebrew后&#xff0c;会在/usr/local/share/生成一个zsh文件夹&#xff0c;里面包含了 因此&#xff0c;zsh文件默认设置的权限是775&#xff0c;也就是group user有writer的权利&#xff0c;zsh认为这是…

centos下./configure报错:Permission denied

./configure 文章目录 ./configure报错解决方案使用chmod给./configure赋予x权限sftp给configure文件赋予x权限 ./configure报错 -bash: ./configure: Permission denied解决方案 使用chmod给./configure赋予x权限 sudo chmod x ./configuresftp给configure文件赋予x权限

webrtc源码阅读之h264 RTP打包

本文来分析webrtc打包h264 rtp包的代码&#xff0c;版本m98 一、RTP协议 1.1 RTP协议概述 实时传输协议&#xff08;RTP&#xff09;是一个网络协议&#xff0c;它允许在网络上进行实时的音频和视频数据传输。RTP协议主要用于解决多媒体数据的实时传输问题&#xff0c;特别是…

React + TypeScript 实践

主要内容包括准备知识、如何引入 React、函数式组件的声明方式、Hooks、useRef<T>、useEffect、useMemo<T> / useCallback<T>、自定义 Hooks、默认属性 defaultProps、Types or Interfaces、获取未导出的 Type、Props、常用 Props ts 类型、常用 React 属性类…