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

目录

一、什么是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方法即可。

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

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

相关文章

游戏行业需要堡垒机吗?用哪款堡垒机好?

相信大家对于游戏都不陌生&#xff0c;上到老&#xff0c;下到小&#xff0c;越来越多的小伙伴开始玩游戏。随着游戏用户的增加&#xff0c;如何保障用户资料安全&#xff0c;如何确保游戏公司数据安全等是一个不容忽视的问题。因此不少人在问&#xff0c;游戏行业需要堡垒机吗…

数据结构----完全二叉树的时间复杂度讲解,堆排序

目录 一.建堆的时间复杂度 1.向上调整算法建堆 2.向下调整算法建堆 二.堆排序 1.概念 2.代码思路 3.代码实现 一.建堆的时间复杂度 1.向上调整算法建堆 我们就以极端情况考虑时间复杂度(满二叉树遍历所有层) 假设所有节点个数为N,树的高度为h N 2^02^12^2......2^(h-…

表的连接【MySQL】

文章目录 什么是连接测试表内连接外连接左外连接右外连接全外连接 自然连接交叉连接参考资料 什么是连接 数据库的连接是指在数据库系统中&#xff0c;两个或多个数据表之间建立的关联关系&#xff0c;使它们可以进行数据的交互和操作。连接通常基于某种共同的字段或条件&…

2.1_2 数据通信基础知识

文章目录 2.1_2 数据通信基础知识&#xff08;一&#xff09;典型的数据通信模型&#xff08;二&#xff09;数据通信相关术语&#xff08;三&#xff09;设计数据通信系统要考虑的3个问题&#xff08;1&#xff09;三种通信方式&#xff08;2&#xff09;串行传输 & 并行传…

开源的python 游戏开发库介绍

本文将为您详细讲解开源的 Python 游戏开发库&#xff0c;以及它们的特点、区别和应用场景。Python 社区提供了多种游戏开发库&#xff0c;这些库可以帮助您在 Python 应用程序中实现游戏逻辑、图形渲染、声音处理等功能。 1. Pygame 特点 - 基于 Python 的游戏开发库。…

C语言分析基础排序算法——交换排序

目录 交换排序 冒泡排序 快速排序 Hoare版本快速排序 挖坑法快速排序 前后指针法快速排序 快速排序优化 快速排序非递归版 交换排序 冒泡排序 见C语言基础知识指针部分博客C语言指针-CSDN博客 快速排序 Hoare版本快速排序 Hoare版本快速排序的过程类似于二叉树前序…

程序员常用小工具推荐

前言 工作或者学习时&#xff0c;常常有一些工具能帮到我们很多&#xff0c;本次简单列举和说明&#xff0c;如果有更多更好用的&#xff0c;欢迎讨论补充。 工具大全 网络分析工具 Wireshark,可以很清晰的解析和过滤网络包&#xff0c;也有助于分析网络的的传输原理。linux环…

基于FPGA的HyperRam接口设计与实现

一 HyperRAM 针对一些低功耗、低带宽应用&#xff08;物联网、消费产品、汽车和工业应用等&#xff09;&#xff0c;涉及到外部存储&#xff0c;HyperRAM提供了更简洁的内存解决方案。 HyperRAM具有以下特性&#xff1a; 1、超低功耗&#xff1a;200MHz工作频率下读写不到50mW…

新书速览|Vue.js 3.x+Element Plus从入门到精通(视频教学版)

详解Vue.jsElement Plus框架各组件的用法&#xff0c;实战网上商城系统和图书借阅系统开发 本书内容 《Vue.js 3.xElement Plus从入门到精通&#xff1a;视频教学版》通过对Vue.js&#xff08;简称Vue&#xff09;的示例和综合案例的介绍与演练&#xff0c;使读者快速掌握Vue.j…

计算机网络—eNSP搭建基础 IP网络

目录 1.下载eNSP 2.启动eNSP 3.建立拓扑 4.建立一条物理连接 5.进入终端系统配置界面 6.配置终端系统 7.启动终端系统设备 8.捕获接口报文 9.生成接口流量 10.观察捕获的报文 1.下载eNSP 网上有许多下载eNSP的方式&#xff0c;记得还要下其它三个Virtual Box、Winpa…

HSCCTF 3th 2024 Web方向 题解wp

WEB-CHECKIN【*没出】 直接给了源码 <?php highlight_file(__FILE__); error_reporting(0); $a$_POST[1]; $b"php://filter/$a/resource/dev/null"; if(file_get_contents($b)"2024"){echo file_get_contents(/flag); }else{echo $b; }咋这么像 WEB…

python文件组织:包(package)、模块(module)、文件(file)

包&#xff1a; 模块所在的包&#xff0c;创建一个包用于组织多个模块&#xff0c;包文件夹中必须创建一个名为’__init__.py’的文件&#xff0c;以将其识别为包&#xff0c;否则只能算作是一个普通的目录。在使用该包时&#xff0c;init自动执行。 包可以多层嵌套&#xff…

使用 ReclaiMe Pro 进行 RAIDZ 数据恢复

天津鸿萌科贸发展有限公司是 ReclaiMe Pro 数据恢复软件授权代理商。 ZFS 是一个开源文件系统&#xff0c;主要用于 FreeNAS 和 NAS4Free 存储系统。在开发 ZFS 时&#xff0c;主要目标是可靠性&#xff0c;这是通过写时复制、冗余元数据、日志等不同功能来实现的。ZFS 使用自…

Redis核心数据结构之跳跃表

跳跃表 概述 跳跃表(skiplist)是一种有序数据结构&#xff0c;它通过在每个节点中维持多个指向其他节点的指针&#xff0c;从而达到快速访问节点的目的。跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找&#xff0c;还可以通过顺序性操作来批量处理节点。在大部分情况下&am…

VB 数据质量诊断软件(分析数据的完整性,合理性,准确性)-139-(代码+程序说明)

转载地址http://www.3q2008.com/soft/search.asp?keyword139 前言: 为何口出狂言,作任何VB和ASP的系统, 这个就是很好的一个证明 :) 又有些狂了... 数据库操作谁都会,接触的多了也没什么难的,VB编程难在哪?算法上,这个是一个算法题的毕业设计 哈哈忙活了足足有一○小时, …

C++进阶之路---多态(一)

顾得泉&#xff1a;个人主页 个人专栏&#xff1a;《Linux操作系统》 《C从入门到精通》 《LeedCode刷题》 键盘敲烂&#xff0c;年薪百万&#xff01; 一、多态的概念 1.概念 多态的概念&#xff1a;通俗来说&#xff0c;就是多种形态&#xff0c;具体点就是去完成某个行为…

IPSec NAT穿越原理

一、IPSec VPN在NAT场景中存在的问题 当某些组网中&#xff0c;有的分支连动态的公网IP地址也没有&#xff0c;只能由网络中的NAT设备进行地址转换&#xff0c;才能访问互联网&#xff0c;然而IPsec是用来保护报文不被修改的&#xff0c;而NAT需要修改报文的IP地址&#xff0c…

9、组合模式(结构性模式)

组合模式又叫部分整体模式&#xff0c;它创建了对象组的树形结构&#xff0c;将对象组合成树状结构&#xff0c;以一致的方式处理叶子对象以及组合对象&#xff0c;不以层次高低定义类&#xff0c;都是结点类 一、传统组合模式 举例&#xff0c;大学、学院、系&#xff0c;它们…

崇法致行法律知识竞赛活动方案

赛程安排分两天&#xff0c;两场进行。 第一天&#xff08;第一场&#xff09;&#xff08;初赛&#xff09; 共 16 个二级分行&#xff0c;每行三人&#xff0c;共16 个战队参赛。 第一轮——必答轮 在大屏幕上显示10个选择题&#xff08;5个单选、5个多选&#xff09;&…

docker安装ollama

拉取镜像 docker pull ollama/ollama 运行容器 &#xff08;挂载路径 D:\ollama 改成你自己喜欢的路径&#xff09; CPU only docker run -d -v D:\ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama Nvidia GPU&#xff08;没试过这个&#xff09; doc…