当前位置: 首页 > article >正文

[高阶数据结构二] LRU Cache详解

1.前言

本章着重介绍LRU Cache机制,LRU Cache的概念,以及如何模拟实现一个LRU Cache。

2.什么是LRU Cache

LRU是Least Recently Used的缩写,意思是最近最少使用,它是一种Cache替换算法

那么什么又是Cache算法呢?他是干嘛的呢?

狭义的Cache指的是位于CPU和主存间的快速RAM, 通常它不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术,广义上的Cache指的是位于速度相差较大的两种硬件之间, 用于协调两者数据传输速度差异的结构。除了CPU与主存之间有Cache, 内存与硬盘之间也有Cache,乃至在硬盘与网络之间也有某种意义上的Cache── 称为Internet临时文件夹或网络内容缓存等


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

3.LRU Cache的实现

实现LRU cache有很多种方式, 但是这里需要做到cache的增删查改都为O(1). 我们可以直接看力扣上面的这道题: LRU cache 我们需要实现两个接口: put和get。下面根据这个题目来对LRU Cache的相关实现做一个介绍。

学过数据结构的都知道,一听到O(1)那么就很容易想到哈希表的增加,修改,查找和删除都是O(1),但是问题来了,LRU Cache还需要知道的是最近少使用,那么哈希表就无法知道哪一个是最近少使用的了。那么我们可以再加一个数据结构,链表或者数组,排在后面的那就默认是少使用的,排在前面的那么就是经常使用的。此时我们知道,无论是链表还是数组,都无法满足增删查改的时间复杂度同时为O(1),链表的增删是O(1),但是查找和修改是O(N),数组则相反。因此单独使用哈希表或者链表都是无法满足要求的。

那么我们就联想到是否能把两者结合呢?--经过分析是可以的。

以链表为例:

实现 LRU Cache 的方法和思路很多,但是要保持高效实现 O(1) put get ,那么使用 双向链表和 哈希表的搭配 是最高效和经典的。使用双向链表是因为双向链表可以实现任意位置O(1) 的插入和 删除,使用哈希表是因为哈希表的增删查改也是O(1)

哈希表里面的第二个元素放的是链表的指针

LRU缓存OJ:

        

解释如下:讲解了如下几种情况,下面给出的是常用的是放在末尾,不常用的是在头部。题目描述是常用的放在头部,不常用的放在尾部。

注意:当你修改或者读取了,也是你使用过了,所以你需要把这个位置先删除,然后重新放到末尾去

解释一下上述过程是如何体现最少使用的。

首先经常使用的是一直放在链表的末尾的,而对于不经常使用的就放在链表的头部,因此最不经常使用的那么一定是在链表的头部,如果容量满了,只需要删除头部的链表元素,然后在尾部添加新增的链表元素即可。这样就体现出了最少使用的思想。

重新回归到题目上来

在返回1那一步其实缓存变成了{2=2,1=1};然后到了增加3那一步时,就会把2删掉。

链表中splice函数介绍

splice函数用于两个list容器之间的拼接(数据转移),其有三种拼接方式:

  1. 将容器当中的某一个数据拼接到另一个容器的指定迭代器位置。
  2. 将整个容器拼接到另一个容器的指定迭代器位置。
  3. 将容器指定迭代器区间的数据拼接到另一个容器的指定迭代器位置

使用方法可参照网站:

list::splice - C++ Reference (cplusplus.com)

LRU Cache的实现

前期铺垫已经做好了,直接上代码了

class LRUCache {
public:
    LRUCache(int capacity) {
        _capacity=capacity;
    }
    
    int get(int key) {
        //得到元素
        auto iter=_umap.find(key);//_umap里面的迭代器
        if(iter!=_umap.end())
        {
            auto it=iter->second;//链表的指针

            //更新常用的2种方法--1.先把节点里面的值记录下来,然后删除。在创建一个新的
            //放在链表头部--这里要注意迭代器失效的问题
            //2.直接用splice函数,拼接,不存在迭代器失效的问题,只是++之后值不同了
            _l.splice(_l.begin(),_l,it);
            return it->second;
        }
        else return -1;
    }
    
    void put(int key, int value) {
        //先找
        auto iter=_umap.find(key);
        if(iter==_umap.end())
        {
            //新增
            if(_capacity==_umap.size())
            {
                //删除尾部不常用
                pair<int,int> back=_l.back();
                _umap.erase(back.first);
                _l.pop_back();
            }
            //然后开始放到头部
            _l.push_front({key,value});
            _umap[key]=_l.begin();//添加映射指向
        }
        else
        {
            //只是修改值--但是要把这个修改的值,然后把这个节点放到链表的最开头
            auto it=iter->second;
            it->second=value;
            _l.splice(_l.begin(),_l,it);
        }
    }
private:
    typedef list<pair<int,int>>::iterator LIST;
    unordered_map<int,LIST> _umap;
    list<pair<int,int>> _l;
    int _capacity;
};

4.总结

LRU知道了结构之后,实现起来并不是很难,但是依然需要对常用的一些数据结构有一定的了解,否则就无法看懂LRU Cache到底在说些什么。


http://www.kler.cn/a/410587.html

相关文章:

  • [每日一氵] 拆分 pip install git+https://github.com/xxx/xx.git@bece3d4
  • ARM CCA机密计算安全模型之概述
  • 【初阶数据结构和算法】leetcode刷题之设计循环队列
  • repmgr安装及常用运维指令
  • RabbitMQ 之 死信队列
  • 速盾:ddos防御手段哪种比较好?高防cdn怎么样?
  • C语言数据结构——详细讲解 双链表
  • Axure PR 9 二级滑动选择器 设计交互
  • ubuntu 安装 docker 记录
  • MySQL:基础操作(增删查改)
  • 从源码到平台:基于第三方视频美颜SDK开发实时直播美颜系统
  • SpringBoot(9)-Dubbo+Zookeeper
  • 使用LLaMA-Factory微调时的问题与解决方案记录
  • Altium Designer学习笔记 16-20 PCB封装调用_3D封装_网表导入常见问题
  • 详解Qt之QtMath Qt数学类
  • seacms 远程命令执行 (CNVD-2020-22721)
  • 将django+vue项目发布部署到服务器
  • SpringBoot开发——Maven多模块工程最佳实践及详细示例
  • 图像处理学习笔记-20241118
  • 11.22 深度学习-pytorch自动微分
  • Android Configuration相关
  • 戴尔 AI Factory 上的 Agentic RAG 搭载 NVIDIA 和 Elasticsearch 向量数据库
  • 基于SpringBoot实现的民宿管理系统(代码+论文)
  • 11超全局变量php
  • 10、PyTorch autograd使用教程
  • redis的List底层数据结构 分别什么时候使用双向链表(Doubly Linked List)和压缩列表(ZipList)