目录
1. LRU_Cache的概念
LRU是Least Recently Used的缩写,意思是最近最少使用,它是一种Cache替换算法。
Cache的概念:
狭义的Cache指的是位于CPU和主存间的快速RAM, 通常它不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术,广义上的Cache指的是位于速度相差较大的两种硬件之间, 用于协调两者数据传输速度差异的结构。除了CPU与主存之间有Cache, 内存与硬盘之间也有Cache,乃至在硬盘与网络之间也有某种意义上的Cache:称为Internet临时文件夹或网络内容缓存等
Cache的容量有限,因此当Cache的容量用完后,而又有新的内容需要添加进来时, 就需要挑选并舍弃原有的部分内容,从而腾出空间来放新内容。LRU_Cache的替换原则就是将最近最少使用的内容替换掉。其实LRU译成最久未使用会更形象, 因为该算法每次替换掉的就是一段时间内最久没有使用过的内容。
百度百科:
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
最近最少使用算法(LRU)是大部分操作系统为最大化页面命中率而广泛采用的一种页面置换算法。该算法的思路是,发生缺页中断时,选择未使用时间最长的页面置换出去。 [1]从程序运行的原理来看,最近最少使用算法是比较接近理想的一种页面置换算法,这种算法既充分利用了内存中页面调用的历史信息,又正确反映了程序的局部问题。
2. LRU_Cache的实现
实现LRU Cache的方法和思路很多,但是要保持高效实现O(1)的put和get,那么使用双向链表和哈希表的搭配是最高效和经典的
使用双向链表是因为双向链表可以实现任意位置O(1)的插入和删除,使用哈希表是因为哈希表的增删查改也是O(1)。
下面通过一道题讲解LRU_Cache的实现。
2.1 力扣146. LRU 缓存
难度 中等
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache
类:
LRUCache(int capacity)
以 正整数 作为容量capacity
初始化 LRU 缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字key
已经存在,则变更其数据值value
;如果不存在,则向缓存中插入该组key-value
。如果插入操作导致关键字数量超过capacity
,则应该 逐出 最久未使用的关键字。
函数 get
和 put
必须以 O(1)
的平均时间复杂度运行。
示例:
输入 ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]] 输出 [null, null, null, 1, null, -1, null, -1, 3, 4] 解释 LRUCache lRUCache = new LRUCache(2); lRUCache.put(1, 1); // 缓存是 {1=1} lRUCache.put(2, 2); // 缓存是 {1=1, 2=2} lRUCache.get(1); // 返回 1 lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3} lRUCache.get(2); // 返回 -1 (未找到) lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3} lRUCache.get(1); // 返回 -1 (未找到) lRUCache.get(3); // 返回 3 lRUCache.get(4); // 返回 4
提示:
1 <= capacity <= 3000
0 <= key <= 10000
0 <= value <= 10^5
- 最多调用
2 * 10^5
次get
和put
class LRUCache {
public:
LRUCache(int capacity) {
}
int get(int key) {
}
void put(int key, int value) {
}
};
/**
* 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);
*/
2.2 LRU结构设计分析
首先想到的实现结构是这样的(能做到O(1)吗):
private:
unordered_map<int, int> _hashMap; // hash能做到查找get更新是O(1)
list<pair<int, int>> _LRUList; // 假设尾部数据就是最近少用的,最近使用的放在头部
size_t _capacity; // 标记容量,满了就弹出链表尾的元素+在哈希表删除该元素的键值对
此时:
int get(int key) {}
void put(int key, int value) {}
- get:直接查map,时间复杂度是O(1)。
- put:如果是新增元素就是O(1),直接插入。但如果是修改更新:就是O(N),因为要在map查找完成后,在_LRUList中找到该元素key所在位置,只能遍历查找,时间复杂度是O(N),然后把该元素key放在头部:O(1)。
破局点:找到key之后,就要找到key对应存储的数据在链表中的位置。
所以:_hashMap中可以存list的迭代器, 之前实现过list,所以知道list的迭代器本质是节点的指针。即:
private:
unordered_map<int, list<pair<int, int>>::iterator> _hashMap; // hash能做到查找get更新是O(1)
list<pair<int, int>> _LRUList; // 假设尾部数据就是最近少用的,最近使用的放在头部
size_t _capacity; // 标记容量,满了就弹出链表尾的元素+在哈希表删除该元素的键值对
这个结构相当于真正的数据在list里面,在map中查找key,就可以直接在链表中找到这个节点,,然后把这个节点转移到链表头部。
2.3 get函数的实现
- 在map中查找key是否存在,如果不存在,返回-1。
- 如果存在,把该key放到list的头部 -> 转移节点,然后返回key对应的值元素存在list里面, _hashMap中的key值:list的迭代器。
如果key对应的值存在,则取出迭代器,这里就可以看出hashmap的value存的是list的iterator的好处:找到key 也就找到key存的值在list中的iterator,也就直接删除,再进行头插,实现O(1)的数据挪动。
转移节点可以用到list的splice接口:splice函数用于两个list容器之间的拼接(数据转移)),其有三种拼接方式:
- 将整个容器拼接到另一个容器的指定迭代器位置。
- 将容器当中的某一个数据拼接到另一个容器的指定迭代器位置。
- 将容器指定迭代器区间的数据拼接到另一个容器的指定迭代器位置。
容器当中被拼接到另一个容器的数据在原容器当中就不存在了。(实际上就是将链表当中的指定结点拼接到了另一个容器当中)。
get函数代码如下:
int get(int key)
{
//_hashMap中的key: key value:list的迭代器(LtIter)
auto ret = _hashMap.find(key);//先查找是否在哈希表中
if(ret != _hashMap.end()) // 存在
{
// 更新当前元素到链表头部
LtIter it= ret->second; // 该元素在list中的位置
// 把元素转移到链表头部,相当于头插,但是这这里不用更新迭代器,因为迭代器里面存的还是这个节点的指针
_LRUList.splice(_LRUList.begin(), _LRUList, it);
//it:是链表的迭代器,调用operator->,返回数据的地址,list中的数据是pair,所以返回的是pair*
//pair<int,int> 我们要的是第二个成员的值,本来应该是it->->second 但是省略了一个箭头
return it->second; // 返回key对应的value
}
else // 该元素不存在
{
return -1;
}
}
2.4 put函数的实现
- 首先在map中查找key是否存在,如果不存在,那就要新增:
- 判断是否满了,如果满了,需要先删除list中尾部的数据,然后在哈希表中也要删除该元素。
- 然后把该 {key-value}头插到链表中 , 在哈希表中新增键值对{key-该位置在链表的迭代器}。
需要注意的是,判断容量的时候,最好不要使用求list的大小来判断, 因为C++中,这个方法可能不是O(1),直接用哈希表的大小就行了。
- 如果已经存在了,那就修改数据,然后把该key放到list的头部。
put函数代码如下:
void put(int key, int value)
{
auto ret = _hashMap.find(key);
if(ret == _hashMap.end()) // 不存在->新增
{
//判断容量的时候,最好不要使用求list的大小来判断,因为C++中,这个方法可能不是O(1)
//if(_capacity == _LRUList.size()) 不建议
if(_capacity == _hashMap.size()) // 容量满了
{
pair<int, int> back = _LRUList.back(); // 取出链表尾部的数据
//在哈希表中删除该数据 + 在链表弹出该元素
_hashMap.erase(back.first);//back.first是key
_LRUList.pop_back();
}
//头插入当前元素 + 哈希表新增键值对{key-该元素在list的位置(链表头部)}
_LRUList.push_front(make_pair(key, value));
_hashMap[key] = _LRUList.begin();
}
else
{
// 修改节点的值 + 当前数据放到链表头部
auto it = ret->second; // it是list的迭代器
it->second = value; // 更新节点的值
_LRUList.splice(_LRUList.begin(),_LRUList,it); // 把当前节点转移到头部
}
}
2.5 OJ完整代码
class LRUCache {
private:
unordered_map<int, list<pair<int, int>>::iterator> _hashMap; // hash能做到查找get更新是O(1)
list<pair<int, int>> _LRUList; // 假设尾部数据就是最近少用的,最近使用的放在头部
size_t _capacity; // 标记容量,满了就弹出链表尾的元素+在哈希表删除该元素的键值对
public:
LRUCache(int capacity)
: _capacity(capacity)
{}
int get(int key)
{
//_hashMap中的key: key value:list的迭代器(LtIter)
auto ret = _hashMap.find(key);//先查找是否在哈希表中
if(ret != _hashMap.end()) // 存在
{
// 更新当前元素到链表头部
auto it= ret->second; // 该元素在list中的位置
// 把元素转移到链表头部,相当于头插,但是这这里不用更新迭代器,因为迭代器里面存的还是这个节点的指针
_LRUList.splice(_LRUList.begin(), _LRUList, it);
//it:是链表的迭代器,调用operator->,返回数据的地址,list中的数据是pair,所以返回的是pair*
//pair<int,int> 我们要的是第二个成员的值,本来应该是it->->second 但是省略了一个箭头
return it->second; // 返回key对应的value
}
else // 该元素不存在
{
return -1;
}
}
void put(int key, int value)
{
auto ret = _hashMap.find(key);
if(ret == _hashMap.end()) // 不存在->新增
{
//判断容量的时候,最好不要使用求list的大小来判断,因为C++中,这个方法可能不是O(1)
//if(_capacity == _LRUList.size()) 不建议
if(_capacity == _hashMap.size()) // 容量满了
{
pair<int, int> back = _LRUList.back(); // 取出链表尾部的数据
//在哈希表中删除该数据 + 在链表弹出该元素
_hashMap.erase(back.first);//back.first是key
_LRUList.pop_back();
}
//头插入当前元素 + 哈希表新增键值对{key-该元素在list的位置(链表头部)}
_LRUList.push_front(make_pair(key, value));
_hashMap[key] = _LRUList.begin();
}
else
{
// 修改节点的值 + 当前数据放到链表头部
auto it = ret->second; // it是list的迭代器
it->second = value; // 更新节点的值
_LRUList.splice(_LRUList.begin(),_LRUList,it); // 把当前节点转移到头部
}
}
};
/**
* 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);
*/
本篇完。
下一篇是其它高阶数据结构⑤_B树(概念+实现OJ)