【深入理解LRU Cache】:缓存算法的经典之作

news2024/11/16 7:42:24

目录

一、什么是LRU Cache?

二、LRU Cache的实现

1.JDK中类似LRUCahe的数据结构LinkedHashMap

2.自己实现双向链表

三、LRU Cache的OJ


一、什么是LRU Cache?

LRU Cache(Least Recently Used的缩写,即最近最少使用,它是一种Cache的替换算法。看Cache替换算法这篇文章)是一种常见的缓存淘汰算法。用于在有限的缓存空间中管理数据对象。LRU Cache 的核心思想是基于时间局部性原理,即最近被访问的数据在未来可能会被再次访问。

Cache的容量有限,因此当Cache的容量用完后,而又有新的内容需要添加进来时, 就需要挑选并舍弃原有 的部分内容,从而腾出空间来放新内容。LRU Cache 的替换原则就是将最近最少使用的内容替换掉。其实, LRU译成最久未使用会更形象, 因为该算法每次替换掉的就是一段时间内最久没有使用过的内容。

注意:LRU Cache 应该更准确地归类为一种缓存淘汰算法,而非传统意义上的数据结构。尽管 LRU Cache 在实现时通常会利用数据结构(如双向链表和哈希表),但它本身更像是一种策略,用于管理缓存中的数据对象。

二、LRU Cache的实现

实现LRU Cache的方法和思路很多,但是要保持高效实现O(1)的put和get,那么使用双向链表和哈希表的搭配是最高效和经典的。

使用双向链表是因为双向链表可以实现任意位置O(1)的插入和删除,双向链表用于记录数据对象的访问顺序,每当一个数据对象被访问时,就将其移动到链表的头部。这样,链表头部的数据对象就是最近被访问的数据,而链表尾部的数据对象则是最久未被访问的数据。同时,使用哈希表能够以 O(1) 的时间复杂度进行数据对象的查找。

当缓存空间达到上限时,需要淘汰最久未被访问的数据对象。这时只需从链表尾部删除相应的数据对象,并在哈希表中删除对应的索引即可。

1.JDK中类似LRUCahe的数据结构LinkedHashMap

LinkedHashMap中有一个这样的构造方法:

重点的accessOrder:

accessOrder 是一个 boolean 类型的参数,用于指定是否按照访问顺序来排序条目。当accessOrder 被设置为 true 时,表示按照访问顺序排序条目;当 accessOrder 被设置为 false 或未指定时(默认情况下),则按照插入顺序排序条目。

示例1:当accessOrder的值为false的时候

输出结果:

{1=a, 2=b, 4=e, 3=c}
以上结果按照插入顺序进行打印。
示例2:当accessOrder的值为true的时候
输出结果:
{4=e, 3=c, 1=a, 2=b}
每次使用get方法,访问数据后,会把数据放到当前双向链表的最后。
当accessOrder为true时,get方法和put方法都会调用recordAccess方法使得最近使用的Entry移到双向链表的末尾;当accessOrder为默认值false时,从源码中可以看出recordAccess方法什么也不会做。
所以要实现一个LRU Cache,最简单的策略就是用自定义类继承LinkedHashMap,然后根据不同要求重写部分方法( 必须重写removeEldestEntry这个方法,默认是false )。
import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache extends LinkedHashMap<Integer, Integer> {

    private final int capacity;

    public LRUCache(int capacity) {
        //accessOrder设置为false时,会按照插入顺序进行排序,当accessOrder为true时,会按照访问顺序
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    @Override
    public Integer get(Object key) {
        //如果get不到,返回默认值-1
        return super.getOrDefault(key, -1);
    }

    @Override
    public Integer put(Integer key, Integer value) {
        return super.put(key, value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        //如果集合内元素个数超过capacity,会将最不常用的元素出队,并将新元素插在尾部
        return size() > capacity;
    }
}

测试:

当然在面试时这个做法肯定不会符合面试官的要求,更好的做法是自己实现一个双向链表。

2.自己实现双向链表

前面说到:

  • 双向链表用于记录数据对象的访问顺序,每当一个数据对象被访问时,就将其移动到链表的头部。这样,链表头部的数据对象就是最近被访问的数据,而链表尾部的数据对象则是最久未被访问的数据。同时,使用哈希表能够以 O(1) 的时间复杂度进行数据对象的查找。
  • 当缓存空间达到上限时,需要淘汰最久未被访问的数据对象。这时只需从链表尾部删除相应的数据对象,并在哈希表中删除对应的索引即可。

为方便测试,我重写了toString方法,测试代码也包含在里面,根据需求删除即可。

import java.util.HashMap;
import java.util.Map;

//双向链表 + 哈希表
public class MyLRUCache {

    //双向链表的节点类
    static class DLinkedNode {
        //键值对
        int key;
        int val;
        DLinkedNode next;
        DLinkedNode prev;

        public DLinkedNode() {
        }

        public DLinkedNode(int key, int val) {
            this.key = key;
            this.val = val;
        }
    }

    private Map<Integer, DLinkedNode> cache; //缓存
    private final int capacity;
    private int size;
    //虚拟的头尾节点(哨兵)
    private DLinkedNode head;
    private DLinkedNode tail;

    public MyLRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new HashMap<>();
        //连接虚拟头尾节点
        this.head = new DLinkedNode();
        this.tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    //插入数据
    public void put(int key, int value) {
        //两种情况:元素是否存在
        if (!cache.containsKey(key)) {
            //1.元素不存在
            //1.1 新元素加到cache集合中
            DLinkedNode newNode = new DLinkedNode(key, value);
            cache.put(key, newNode);
            //1.2 插到尾部
            addLast(newNode);
            size++;
            //1.3 判断当前的元素个数是否超过容量
            if (size > capacity) {
                //1.4 删除第一个节点,即最近最久未使用
                DLinkedNode del = removeFirst();
                //同时需要把该元素从cache中删除
                cache.remove(del.key);
                size--;
            }
        } else {
            //2.元素存在,修改链表的顺序,将该节点放到尾部(最近一次使用的)
            DLinkedNode node = cache.get(key);
            //2.1 以新的值将该节点移动到尾部
            node.val = value;
            moveLast(node);
        }
    }

    //将节点移动到尾部
    private void moveLast(DLinkedNode node) {
        //1.删除当前节点
        removeNode(node);
        //2.尾插到链表
        addLast(node);
    }

    //删除节点
    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    //删除第一个节点
    private DLinkedNode removeFirst() {
        DLinkedNode del = head.next;
        head.next = del.next;
        head.next.prev = head;
        return del;
    }

    //尾插到链表
    private void addLast(DLinkedNode node) {
        //连接最后一个节点和新的尾节点
        tail.prev.next = node;
        node.prev = tail.prev;
        //连接新尾节点和傀儡尾节点
        tail.prev = node;
        node.next = tail;
    }

    //获取数据
    public int get(int key) {
        //1.cache集合中拿到这个节点
        DLinkedNode node = cache.get(key);
        //2.判断是否有这个节点
        if (node != null) {
            //2.1 有该节点 将该节点移动到尾部(最近一次使用的)
            moveLast(node);
            return node.val;
        }
        //2.2 没有该节点,返回-1
        return -1;
    }

    //删除数据
    public boolean remove(int key) {
        //1.看缓存中存不存在
        DLinkedNode node = cache.get(key);
        //2.不存在,直接返回
        if (node == null) {
            return false;
        }
        //3.存在
        //3.1 删除链表的节点
        removeNode(node);
        //3.2 删除cache集合的数据
        cache.remove(key);
        size--;
        return true;
    }

    @Override
    public String toString() {
        StringBuilder sbu = new StringBuilder();
        DLinkedNode cur = head.next;
        sbu.append("{");
        while (cur != tail) {
            if (cur.next != tail) {
                sbu.append(cur.key).append("=").append(cur.val).append(", ");
            } else {
                sbu.append(cur.key).append("=").append(cur.val);
            }
            cur = cur.next;
        }
        sbu.append("}");
        return sbu.toString();
    }

    public static void main(String[] args) {
        MyLRUCache lruCache = new MyLRUCache(3);
        lruCache.put(1, 10);
        lruCache.put(2, 11);
        lruCache.put(3, 12);
        System.out.println(lruCache);
        System.out.println("使用后:");
        lruCache.get(2);
        System.out.println(lruCache);
        lruCache.get(1);
        System.out.println(lruCache);
        lruCache.put(4, 13);
        System.out.println("最不常用的被删除,新元素插到尾部:");
        System.out.println(lruCache);
    }
}

三、LRU CacheOJ

LeetCode 热题100 链表专题的最后一题

146. LRU 缓存 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/lru-cache/solutions/259678/lruhuan-cun-ji-zhi-by-leetcode-solution/?envType=study-plan-v2&envId=top-100-liked前面两种实现中,MyLRUCache的get方法在获取不到数据时返回的是-1的原因就是根据这道题的要求做的,并且多写了一个remove方法。提交时将类名和构造方法改成原题默认的LRUCache,且不要main方法即可。

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

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

相关文章

python INI文件操作与configparser内置库

目录 INI文件 configparser内置库 类与方法 操作实例 导入INI文件 查询所有节的列表 判断某个节是否存在 查询某个节的所有键的列表 判断节下是否存在某个键 增加节点 删除节点 增加节点的键 修改键值 保存修改结果 获取键值 获取节点所有键值 其他读取方式 …

JAVA实战开源项目:超市自助付款系统(Vue+SpringBoot)

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容2.1 商品类型模块2.2 商品模块2.3 超市账单模块 三、界面展示3.1 登录注册模块3.2 超市商品类型模块3.3 超市商品模块3.4 商品购买模块3.5 超市账单模块 四、部分源码展示4.1 实体类定义4.2 控制器接口 五、配套文档展示六、…

【APP逆向】酒仙网预约茅台(附带源码)

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 所属的专栏:爬虫实战,零基础、进阶教学 景天的主页:景天科技苑 文章目录 酒仙网预约抢购茅台1.抓包分析,账户名和密码登录2.短信登录3.登录+茅台预约 密码登录酒仙网预约抢购茅台 目标:账号登…

Fortran语法介绍(三)

个人专栏—ABAQUS专栏 Abaqus2023的用法教程——与VS2022、oneAPI 2024子程序的关联方法 Abaqus2023的用法教程——与VS2022、oneAPI 2024子程序的关联方法Abaqus有限元分析——有限元网格划分基本原则 Abaqus有限元分析——有限元网格划分基本原则各向同性线弹性材料本构模型…

吴恩达深度学习笔记:神经网络的编程基础2.1-2.3

目录 第一门课&#xff1a;神经网络和深度学习 (Neural Networks and Deep Learning)第二周&#xff1a;神经网络的编程基础 (Basics of Neural Network programming)2.1 二分类(Binary Classification)2.2 逻辑回归(Logistic Regression)2.3 逻辑回归的代价函数&#xff08;Lo…

Gin 获取请求参数

POST 请求参数 Gin 获取Post请求URL参数有三种方式 func (c *Context) PostForm(key string) string func (c *Context) DefaultPostForm(key, defaultValue string) string func (c *Context) GetPostForm(key string) (string, bool)大多数情况下使用的是application/x-www…

Electron程序如何在MacOS下获取相册访问权限

1.通过entitiment.plist&#xff0c;在electron-builder签名打包时&#xff0c;给app包打上签名。最后可以通过codesign命令进行验证。 TestPhotos.plist electron-builder配置文件中加上刚刚的plist文件。 通过codesign命令验证&#xff0c;若出现这个&#xff0c;则说明成…

day60 安装MySql数据库

1如何安装MySQL数据库 2数据库概念 3数据库语言 1 SQL&#xff08;数据结构化查询语句&#xff09; 2分类 1数据库查询语言DQL select 2数据库操作语言DML insert&#xff0c;update&#xff0c;delete 3数据库定义语言DDL create&#xff0c;alter修改&am…

MySQL--优化(索引--聚簇和非聚簇索引)

MySQL–优化&#xff08;索引–聚簇和非聚簇索引&#xff09; 定位慢查询SQL执行计划索引 存储引擎索引底层数据结构聚簇和非聚簇索引索引创建原则索引失效场景 SQL优化经验 一、聚簇索引 聚簇索引&#xff1a;将数据存储与索引放到了一块&#xff0c;索引结构的叶子节点保存…

C语言连接【MySQL】

稍等更新图片。。。。 文章目录 安装 MySQL 库连接 MySQLMYSQL 类创建 MySQL 对象连接数据库关闭数据库连接示例 发送命令设置编码格式插入、删除或修改记录查询记录示例 参考资料 安装 MySQL 库 在 CentOS7 下&#xff0c;使用命令安装 MySQL&#xff1a; yum install mysq…

明日周刊-第1期

打算开一个新的专栏&#xff0c;专门记录一周发生的事情以及资源共享&#xff0c;那么就从第一期开始吧。 1. 一周热点 人工智能技术突破&#xff1a;可能会有关于人工智能领域的最新研究成果&#xff0c;例如新算法的开发、机器学习模型的提升或者AI在不同行业的应用案例。 量…

PT:dmsa如何设置don‘t use

我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧? 拾陆楼知识星球入口 往期文章链接: PT: 基于Multi Voltage的physical aware DMSA PT: DMSA remote_execute { \ define_user_attribu

Tomcat源码解析(四):StandardServer和StandardService

Tomcat源码系列文章 Tomcat源码解析(一)&#xff1a;Tomcat整体架构 Tomcat源码解析(二)&#xff1a;Bootstrap和Catalina Tomcat源码解析(三)&#xff1a;LifeCycle生命周期管理 Tomcat源码解析(四)&#xff1a;StandardServer和StandardService 文章目录 前言一、Standar…

大数据赋能,能源企业的智慧转型之路

在数字洪流中&#xff0c;大数据已经成为推动产业升级的新引擎。特别是在能源行业&#xff0c;大数据的应用正引领着一场深刻的智慧转型。今天&#xff0c;我们就来探讨大数据如何在能源企业中发挥其独特的魅力&#xff0c;助力企业提效降本&#xff0c;实现绿色发展。 动态监控…

R语言读取大型NetCDF文件

失踪人口回归&#xff0c;本篇来介绍下R语言读取大型NetCDF文件的一些实践。 1 NetCDF数据简介 先给一段Wiki上关于NetCDF的定义。 NetCDF (Network Common Data Form) is a set of software libraries and self-describing, machine-independent data formats that support…

光线追踪11 - Positionable Camera(可定位相机)

相机和介质一样&#xff0c;调试起来很麻烦&#xff0c;所以我总是逐步开发我的相机。首先&#xff0c;我们允许可调节的视野&#xff08;fov&#xff09;。这是渲染图像从一边到另一边的视觉角度。由于我们的图像不是正方形的&#xff0c;水平和垂直的视野是不同的。我总是使用…

mybatis基础操作(三)

动态sql 通过动态sql实现多条件查询&#xff0c;这里以查询为例&#xff0c;实现动态sql的书写。 创建members表 创建表并插入数据&#xff1a; create table members (member_id int (11),member_nick varchar (60),member_gender char (15),member_age int (11),member_c…

【探索程序员职业赛道:挑战与机遇】

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学习,不断总结,共同进步,活到老学到老导航 檀越剑指大厂系列:全面总结 jav…

微信小程序(五十三)修改用户头像与昵称

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.外界面个人资料基本模块 2.资料修改界面同步问题实现&#xff08;细节挺多&#xff0c;考虑了后期转服务器端的方便之处&#xff09; 源码&#xff1a; app.json {"window": {},"usingCompone…

什么是5G边缘计算网关?

随着5G技术的飞速发展和普及&#xff0c;边缘计算作为5G时代的关键技术之一&#xff0c;正日益受到业界的关注。而5G边缘计算网关&#xff0c;作为连接5G网络和边缘计算节点的桥梁&#xff0c;扮演着至关重要的角色。HiWoo Box&#xff0c;作为一款卓越的5G边缘计算网关&#x…