[高阶数据结构二] 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临时文件夹或网络内容缓存等
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容器之间的拼接(数据转移),其有三种拼接方式:
- 将容器当中的某一个数据拼接到另一个容器的指定迭代器位置。
- 将整个容器拼接到另一个容器的指定迭代器位置。
- 将容器指定迭代器区间的数据拼接到另一个容器的指定迭代器位置
使用方法可参照网站:
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到底在说些什么。