【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,其它元素值+1flag.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){// 缓存容量足够,直接添加,并将新加入元素标记值置为初值0cache.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策略淘汰最旧的值*/@Overrideprotected 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) {// 缓存未命中,直接返回-1return -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 为缓存中元素的最大个数

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue 组件化开发

文章目录 前言组件化开发父子组件相互传数据父传子&#xff1a;自定义属性子传父&#xff1a;自定义事件父子组件互传案例 插槽 slot多个插槽 总结组件化开发总结Vue组件的基本组成子组件使用的三个步骤父子组件相互传递数据 前言 提示&#xff1a;这里可以添加本文要记录的大…

Android-jar包方式连接本地sqlite并操作返回数据

背景: 数据库的创建及字段都是后端人员维护,Android端只是映射相关数据库到本地来操作。为了统一管理操作方式方法,所以提出,后端打jar包的方式封装对Android端数据库sqllite的连接、操作。 说明: 因为之前是后端打jar包,JDBC连接的驱动及方法有差异,导致连接Android…

VMware将虚拟机网络设置为NAT模式

虚拟机有vmware和desktop&#xff0c;本人一直使用的是vmware。安装好vmware并激活后&#xff0c;创建完虚拟机。(需要vmware和激活码的可留言) 进入虚拟机设置&#xff0c;网络适配器选择NAT模式 在虚拟机工具栏->菜单栏点击编辑&#xff0c;选择“虚拟网络编辑器”。 选择…

[C语言][小游戏][猜拳游戏]

C语言的奇妙旅行 一、模块化编程二、游戏基本设计2.1 确定计算机要出的手势2.2 显示“石头剪刀布”&#xff0c;然后玩家输入自己要出的手势2.3进行输赢判断&#xff0c;显示结果2.4询问是否继续2.5 基本程序 三、游戏实现的过程3.1将玩家的手势和电脑的手势显示出来 三、总代码…

metersphere主从节点部署

metersphere主从节点关系 环境搭建 docker 环境准备 检查内存是否大于8G free -m 安装docker服务 安装docker&#xff0c;使用yum -y install docker&#xff1b; 启动docker&#xff0c;使用systemctl start docker&#xff1b; 设置开机启动&#xff0c;使用systemctl en…

基于SpringBoot的网上订餐系统【附ppt和开题|万字文档(LW)和搭建文档】

主要功能 前台登录&#xff1a;前台登录&#xff1a; ①首页&#xff1a;菜品信息推荐、菜品信息展示、查看更多 ②菜品信息&#xff1a;菜品分类、菜品名称查询、食材查询、菜品详情、下单提交 ③个人中心&#xff1a;可以查看自己的信息、我的订单、我的地址 后台登录&#…

哪款3D虚拟人物建模软件好用?

3D虚拟人物建模软件一直以来受到许多人的关注和追捧。现在&#xff0c;随着智能手机的普及&#xff0c;3D虚拟人物手机建模软件也开始走进大家的视野。那么&#xff0c;市面上3D虚拟人物建模软件这么多&#xff0c;究竟哪款3D虚拟人物建模软件是好用的呢&#xff1f; 首先&…

Vision Transformer推理中线性-角度注意转换压缩自注意

文章目录 Castling-ViT: Compressing Self-Attention via Switching Towards Linear-Angular Attention at Vision Transformer Inference摘要本文方法实验结果 Castling-ViT: Compressing Self-Attention via Switching Towards Linear-Angular Attention at Vision Transform…

Blender--原理化体积

“原理化体积 着色器将所有体积着色组件组合到一个易于使用的节点中。该节点含有散射&#xff0c;吸收和黑体辐射属性&#xff0c;因此&#xff0c;可以仅仅使用该着色器节点对烟雾和火焰等进行渲染。” 官方文档介绍&#xff1a;原理化体积 — Blender Manual 可以用于实现丁…

Docker入门

目录&#xff1a; 常见概念评价指标单机架构应用数据分离架构应用服务集群架构读写分离 / 主从分离架构引入缓存 —— 冷热分离架构垂直分库业务拆分 —— 微服务容器化引入——容器编排架构总结 1.常见概念&#xff1a; 应用&#xff08;Application&#xff09; / 系统&am…

mac使用conda(anaconda和miniconda一样)安装新版本的torch

使用pytorch给的命令行下载会很慢&#xff0c;因此我们应该修改镜像源&#xff0c;然后再下载torch 1.添加镜像 在终端输入以下命令&#xff0c;添加镜像&#xff1a; conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/ conda conf…

【STM32MP135】修改10.1寸屏1280x800分辨率配置,解决fb_size过小导致运行崩溃

文件路径&#xff1a;u-boot-stm32mp-v2021.10-stm32mp1-r1/configs/stm32mp13_defconfig