[高阶数据结构二] 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的实现

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

cpp 复制代码
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到底在说些什么。

相关推荐
SkyrimCitadelValinor8 分钟前
python【数据结构】
开发语言·数据结构·python
就爱学编程19 分钟前
重生之我在异世界学编程之数据结构与算法:深入数和二叉树篇
数据结构
_周游42 分钟前
【C语言】_const修饰指针变量
c语言·数据结构·算法
爱学习的徐徐6 小时前
【数据结构】链表链表
数据结构·链表
高 朗9 小时前
【算法刷题】链表
数据结构·算法·链表
走在考研路上9 小时前
力扣459 重复的字符串
数据结构·算法·leetcode
Komorebi_awa10 小时前
xdoj-字符串-556,为什么字符不能被正常读入
c语言·数据结构·算法
一只小bit11 小时前
深入刨析数据结构之排序(下)
c语言·数据结构·算法·排序算法
风间琉璃""12 小时前
PWN的知识之栈溢出
数据结构·算法·网络安全·pwn·二进制安全·栈溢出
HUT_Tyne26513 小时前
力扣--494.目标和
数据结构·算法·leetcode