文章目录
-
- 1.CPU取数据时的分层
- [2.LRU Cache](#2.LRU Cache)
- [3.LRU Cache 的实现](#3.LRU Cache 的实现)
1.CPU取数据时的分层
如下所示是CPU的三级缓存,因为CPU太快了,所以设置了很多层来提高总体的效率

那么这里就涉及到一个问题,缓存空间优先使用的话,如果满了以后,要更新数据,其他数据要进入,谁出去呢?
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的方法和思路很多,但是要保持高效实现O(1)的put和get,那么使用双向链表和哈希表的搭配是最高效和经典的。使用双向链表是因为双向链表可以实现任意位置O(1)的插入和删除,使用哈希表是因为哈希表的增删查改也是O(1)
我们来看这道题
代码实现如下:
cpp
class LRUCache {
public:
LRUCache(int capacity)
:_capacity(capacity)
{}
int get(int key) {
auto ret = _hashmap.find(key);
if(ret != _hashmap.end())
{
LIterator it = ret->second;
//转移迭代器
_LRUList.splice(_LRUList.begin(), _LRUList, it);
return it->second;
}
else
{
return -1;
}
}
void put(int key, int value) {
auto ret = _hashmap.find(key);
//原来没有,要新增了。
if(ret == _hashmap.end())
{
//容量已满,需要先清理
if(_capacity == _hashmap.size())
{
//哈希表的删除要用到对应的key,我们要用的是链表的最后一个key值
//这里获取链表的最后一个结点的key
pair<int, int> back = _LRUList.back();
//删除哈希表的值
_hashmap.erase(back.first);
//链表的尾删
_LRUList.pop_back();
}
//删完后开始添加数据
_LRUList.push_front(make_pair(key, value));
_hashmap[key] = _LRUList.begin();
}
//如果原来就有key,那么先将key所对应的value进行替换掉,然后换一下顺序即可
else
{
//修改一下原来的这个数据
LIterator it = ret->second;
it->second = value;
_LRUList.splice(_LRUList.begin(), _LRUList, it);
}
}
private:
typedef list<pair<int, int>>::iterator LIterator;
unordered_map<int, LIterator> _hashmap;
list<pair<int, int>> _LRUList;
int _capacity;
};
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache* obj = new LRUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/
我们的思路如下所示
首先我们的容器使用list和unordered_map两个,其次需要一个变量_capacity记录当前Cache的容量。因为我们的需求是get函数和push函数的效率都是O(1)。首先哈希表是可以确保它的插入和获取都是O(1)的。但是因为其是无序的,无法实现LRU的思想。所以我们需要使用一个list来进行辅助。对于list,我们可以这样设计,总是让他的最后一个结点是将要被淘汰的。也就是按照使用的时间进行排序。对于这样的思路,我们可以只利用list的头插和尾删进行实现。而这两个的效率都是O(1)。不过问题是如何将这两个容器给关联起来,因为如果不进行关联的话,那么在哈希表中找到数据以后,还要在list中去找到对应的数据将他在它自己的链表中进行头插,此时查找的效率是O(N)了。所以我们可以让数据只存储到list中,也就是
list<pair<int , int>>
的结构让list去存储数据,而后让哈希表中存储的key是正常的key值,但是value值存储的是链表的迭代器。让这个迭代器去指向对应的链表中的数据。这样的话,我们就可以实现,一旦在哈希表中找到该数据存在了,然后迅速找到对应的链表中的内容。去进行处理。如果是获取操作,那么我们可以将链表中的该数据原地头插,然后返回链表中的数据。我们可以发现效率都是O(1)。
如果是push操作
那么先要通过哈希表快速判断该数据有没有,如果不存在的话,那么就是新增操作,此时我们先要检测一下容量是否满了,如果满了,那么先要删除掉链表的最后一个数据以及哈希表中对应的数据,而在删除的时候,我们哈希表并不知道要删除哪个数据,只有链表知道,所以先要删除哈希表,然后删除链表中的数据,通过链表的back接口获取到要删除的key值,然后让哈希表删除以后,链表在进行尾删。删除好了以后就是新增了,由于哈希表的value存储的是链表的迭代器,所以只能先为链表添加,为链表头插以后,然后再将key和链表第一个结点的迭代器给哈希表。自此完成映射。
如果我们一开始哈希表的判断是数据是存在的,即该key值有对应的value,那么此时我们就是一个修改操作,我们可以先通过哈希表找到该迭代器,然后将该迭代器所指向的链表的内容的第二个数据second进行修改。然后将该结点给原地头插。就可以了。
对于链表的原地头插,其实在库里面就有一个splice接口。
cppvoid splice (iterator position, list& x, iterator i);
该接口的含义是,将x链表中的i迭代器转移到position迭代器位置之前。
类似的接口还有如下
cpp//将x链表全部转移到position之前 void splice (iterator position, list& x); //将x链表的[first,last)区间的迭代器放入到position之前 void splice (iterator position, list& x, iterator first, iterator last);