目录
1.什么是LRU Cache?
2.LRU Cache 的底层结构
3.LRU Cache的实现
LRUCache类中的接口总览
构造函数
get操作
put操作
打印
4.LRU Cache的测试
5.LRU Cache相关OJ题
6.LRU Cache类代码附录
1.什么是LRU Cache?
首先我想解释一下什么是cache。所谓cache,其实就是一段缓冲区,位于运行速度相差较大的两种硬件之间, 用于协调两者数据传输速度差异的结构。
比如计算机的存储结构:
我们知道程序都是在磁盘上存储的,但是程序都要放在CPU上执行,CPU的执行速度非常快,而磁盘是外设,代码从外设搬到CPU上的速度又非常慢,此时,CPU大部分时间都在等待代码和数据的到来,这样无疑是对CPU的浪费;于是在外设和CPU之间添加Cache 缓存用于进行预加载;CPU每次不再向磁盘 “索要” 要代码和数据,而是从Cache上拿,CPU还在执行当前的代码和数据时,磁盘上的代码和数据又可以往Cache上加载,于是,便协调了CPU和磁盘之间的数据加载不平衡的问题。
旧的问题解决了,又产生了新的问题:
Cache的容量是有限的,因此当Cache的容量用完后,而又有新的内容需要添加进来时, 就需要挑选并舍弃原有的部分内容,从而腾出空间来放新内容,那应该舍弃那部分内容呢?经过科学家的研究,认为舍弃最近最少使用(Least Recently Used)过的数据是最合理的,也就是舍弃LRU数据。
上面说的都是硬件层的东西,但是需要通过软件层的算法来解决,也就是设计如何舍弃最近最少使用的数据,这便是LRU Cache 替换算法。
2.LRU Cache算法的底层结构
要设计出一个LRU Cache替换算法不难,但是要设计出一个高效的LRU Cache替换算法有难度,高效体现在任意操作的时间复杂度都是O(1)。
说明一下:我们设计的LRU Cache中存储的数据为键值对的形式。
那LRU Cache替换算法都有哪些操作呢?其实也就两个主要操作。
- 一个操作是往Cache中放数据(put操作)。要求:如果关键字
key
已经存在,则变更其数据值value
;如果不存在,则向缓存中插入该组key-value
。如果插入操作导致关键字数量超过capacity
(Cache的容量),则应该 逐出 最久未使用的关键字。 - 一个操作是把数据从Cache中拿出来(get操作)。要求:如果关键字 key 存在于缓存中,则返回关键字的值,否则返回默认值并提示。
如何使这两个操作的时间复杂度都达到O(1)呢?此时,底层数据结构的选择尤为重要。
- 无论是get还是put操作,都需要快速根据关键字的值查找一个元素是否存在于 Cache 中,我们知道,查找一个元素时间复杂度为O(1)的数据结构是哈希表,因此我们可以选择STL中的unordered_map作为其底层结构。
- 但是这还不够,我们需要频繁的进行数据的增加和删除操作,并且要求数据的增加和删除操作的效率都是O(1)。因此,我们还可以借助STL中的 list(带头双向循环链表)来存储数据。
- 同时,因为我们还需要保证Cache满了之后,替换的是最近最少使用的数据,我们可以让链表的末尾存放最近最少使用的数据,每次替换的时候删除末尾的数据即可。为了实现这一点,我们只需要将每次访问过的数据提取到 list 的头部即可,尾部自然而然就是最近最少使用的数据。
LRU Cache的底层存储示意图:
3.LRU Cache算法的实现
LRUCache类中的接口总览
说明一下:我们将LRUCache类设计为模版类,以便适应各种数据类型。
template<class Key, class Val>
class LRUCache
{
public:// 构造函数LRUCache(int capacity):_capacity(capacity){}// 通过key获取对应的valVal get(Key key);// 往Cache中插入<key,value>void put(Key key, Val value);// 输出list中的结点,便于调试分析void print();private:// 对list的迭代器类型进行重命名using LtIte = typename std::list<std::pair<Key, Val>>::iterator;// 保证查找的效率是O(1)std::unordered_map<Key, LtIte> _hashMap;// 保证插入删除数据的效率是O(1)std::list<std::pair<Key, Val>> _LRU_list;// Cache的容量size_t _capacity;
};
构造函数
直接指定LRUCache中的容量即可。
// 构造函数
LRUCache(int capacity):_capacity(capacity)
{}
get操作
- 首先通过哈希表判断key是否存在,如果存在则返回对应的value,并且将访问过的元素提取到list的开头。
- 如果不存在,返回该类型通过默认构造函数构造出的对象即可,并提示该元素不存在。
代码如下所示:
// 通过key获取对应的val
Val get(Key key)
{auto ret = _hashMap.find(key);if (ret != _hashMap.end()) // 获取val的同时,更改key所在结点的位置{// 获取元素在list中的结点LtIte it = ret->second;// 使用list的splice接口将当前访问的结点转移到链表的开头_LRU_list.splice(_LRU_list.begin(), _LRU_list, it);// 返回list结点中valreturn it->second;}std::cout << "该元素不存在" << std::endl;return Val();
}
- 注意:这里将访问过的结点提取到链表的开头也可以通过erase+push_front来实现,但是需要更新迭代器,防止迭代器失效的问题。因此,我们使用操作更加简单的list的splice接口。
put操作
- put数据的时候,需要判断该数据的key值是否存在,如果存在则更新对应的value,如果不存在则插入,并且两种情况都视为访问过当前结点,需要将访问过的结点转移到list的开头位置。
- 在该数据不存在的情况下,需要判断Cache中数据是否满了,满了就要删除list末尾的数据(LRU数据)。
// 往Cache中插入<key,value>
void put(Key key, Val value)
{auto ret = _hashMap.find(key);if (ret == _hashMap.end()) // key不存在,需要插入<key,value>{// 如果满了,就要删除LRU的数据if (_capacity == _hashMap.size()){// 从list中获取尾部的数据std::pair<Key, Val> back_data = _LRU_list.back();// 删除哈希表中对应的数据_hashMap.erase(back_data.first);// 删除list中对应的数据_LRU_list.pop_back();}// 头插新来的<key,value>结点_LRU_list.push_front(std::make_pair(key, value));// 将新数据添加到哈希表中_hashMap[key] = _LRU_list.begin();}else // list中存在key,需要更新key对应的value{// 获取list中结点的迭代器LtIte it = ret->second;// 修改结点中的valueit->second = value;// 将访问过的结点转移到链表的头部_LRU_list.splice(_LRU_list.begin(), _LRU_list, it);}
}
打印
这个接口不是必需的,只是为了检测、调试LRUCache类。
// 打印list中的数据
void print()
{for (auto e : _LRU_list){std::cout << e.first << ":" << e.second << std::endl;}
}
4.LRU Cache算法的测试
测试代码:
#include <string>
#include "LRUCache.h"int main()
{LRUCache<std::string, std::string> lc(5);lc.put("book", "书");lc.put("string", "字符串");lc.put("water", "水");lc.put("computer", "计算机");lc.put("glass", "玻璃");lc.print();std::cout << std::endl;lc.get("water");lc.print();std::cout << std::endl;lc.put("book", "预定");lc.print();std::cout << std::endl;return 0;
}
运行结果:
5.LRU Cache算法相关OJ题
题目链接:
146. LRU 缓存 - 力扣(LeetCode)146. LRU 缓存 - 请你设计并实现一个满足 LRU (最近最少使用) 缓存 [https://baike.baidu.com/item/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); // 返回 1lRUCache.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); // 返回 3lRUCache.get(4); // 返回 4 提示: * 1 <= capacity <= 3000 * 0 <= key <= 10000 * 0 <= value <= 105 * 最多调用 2 * 105 次 get 和 puthttps://leetcode.cn/problems/lru-cache/题目描述:
请你设计并实现一个满足 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
解题思路:
参考本篇博客即可。
运行代码:
class LRUCache {// 对list的迭代器类型进行重命名using LtIte = list<pair<int, int>>::iterator;
public:// 构造函数LRUCache(int capacity):_capacity(capacity){}// 通过key获取对应的valint get(int key){auto ret = _hashMap.find(key);if (ret != _hashMap.end()) // 获取val的同时,更改key所在结点的位置{// 获取元素在list中的结点LtIte it = ret->second;// 使用list的splice接口将当前访问的结点转移到链表的开头_LRU_list.splice(_LRU_list.begin(), _LRU_list, it);// 返回list结点中valreturn it->second;}std::cout << "该元素不存在" << std::endl;return -1;}// 往Cache中插入<key,value>void put(int key, int value){auto ret = _hashMap.find(key);if (ret == _hashMap.end()) // key不存在,需要插入<key,value>{// 如果满了,就要删除LRU的数据if (_capacity == _hashMap.size()){// 从list中获取尾部的数据std::pair<int, int> back_data = _LRU_list.back();// 删除哈希表中对应的数据_hashMap.erase(back_data.first);// 删除list中对应的数据_LRU_list.pop_back();}// 头插新来的<key,value>结点_LRU_list.push_front(std::make_pair(key, value));// 将新数据添加到哈希表中_hashMap[key] = _LRU_list.begin();}else // list中存在key,需要更新key对应的value{// 获取list中结点的迭代器LtIte it = ret->second;// 修改结点中的valueit->second = value;// 将访问过的结点转移到链表的头部_LRU_list.splice(_LRU_list.begin(), _LRU_list, it);}}private:unordered_map<int, LtIte> _hashMap;list<pair<int, int>> _LRU_list;size_t _capacity;
};
运行结果:
6.LRU Cache类代码附录
#include <iostream>
#include <unordered_map>
#include <list>template<class Key, class Val>
class LRUCache
{
public:// 构造函数LRUCache(int capacity):_capacity(capacity){}// 通过key获取对应的valVal get(Key key){auto ret = _hashMap.find(key);if (ret != _hashMap.end()) // 获取val的同时,更改key所在结点的位置{// 获取元素在list中的结点LtIte it = ret->second;// 使用list的splice接口将当前访问的结点转移到链表的开头_LRU_list.splice(_LRU_list.begin(), _LRU_list, it);// 返回list结点中valreturn it->second;}std::cout << "该元素不存在" << std::endl;return Val();}// 往Cache中插入<key,value>void put(Key key, Val value){auto ret = _hashMap.find(key);if (ret == _hashMap.end()) // key不存在,需要插入<key,value>{// 如果满了,就要删除LRU的数据if (_capacity == _hashMap.size()){// 从list中获取尾部的数据std::pair<Key, Val> back_data = _LRU_list.back();// 删除哈希表中对应的数据_hashMap.erase(back_data.first);// 删除list中对应的数据_LRU_list.pop_back();}// 头插新来的<key,value>结点_LRU_list.push_front(std::make_pair(key, value));// 将新数据添加到哈希表中_hashMap[key] = _LRU_list.begin();}else // list中存在key,需要更新key对应的value{// 获取list中结点的迭代器LtIte it = ret->second;// 修改结点中的valueit->second = value;// 将访问过的结点转移到链表的头部_LRU_list.splice(_LRU_list.begin(), _LRU_list, it);}}// 打印list中的数据void print(){for (auto e : _LRU_list){std::cout << e.first << ":" << e.second << std::endl;}}private:// 对list的迭代器类型进行重命名using LtIte = typename std::list<std::pair<Key, Val>>::iterator;// 保证查找的效率是O(1)std::unordered_map<Key, LtIte> _hashMap;// 保证插入删除数据的效率是O(1)std::list<std::pair<Key, Val>> _LRU_list;// Cache的容量size_t _capacity;
};