目录
1 什么是LRU cache
LRU(Least Recently Used,最近最少使用)算法是一种淘汰策略,简单来讲实现的是如下工作:将一些元素放在一个容量固定的容器中进行存取,由于容器的容量有限,该容器就要保证那些最近才被用到的元素始终在容器内,而将已经很久没有用的元素剔除,实现容器内元素的动态维护。这种算法是一种缓存维护策略,因为缓存空间有限,让缓存中存储的都是最近才被用到的元素可以实现系统缓存的高效运作。
2 数据结构与算法设计
2.1 需求
实现一个类LRUCache。
2.2 设计
根据以上要求,我们可以确定:
- LRUCache本身需要用一个具体的存储容器来实现,该容器内部存储的元素形式是键值对的形式,并且该容器查找元素是通过输入键,返回键所对应的值来实现的,因此可以确定,LRUCache容器本身可以使用哈希表这种数据结构来实现。并且根据键与值的参数类型可以确定,该哈希表的键和值都是int类型。
- LRUCache的
int get(int key)
接口完成的操作是哈希表本身就可以完成的操作。同时,该接口定义了缓存中元素被使用的含义,即最近一次通过get接口被查询到的键值对元素是缓存中最近一次被使用到的元素。 - LRUCache的
void put(int key, int value)
接口完成的操作隐含了动态维护LRUCache中元素的需求。动态维护的方法就是LRU算法,即将LRUCache中的元素(int键值对)按照使用的时间顺序排列。同时需要特别注意的是,该接口操作也定义了缓存中元素另一种被使用的含义,即最近一次通过put接口被放入缓存中的元素是缓存中最近一次被使用到的元素。
可以通过双向链表来实现LRU算法,细节如下:
- 双向链表的节点存储内容是用来实现LRUCache容器的哈希表元素(int类型键值对)
- 双向链表存在界限,即最大节点数就是LRUCache的容量。
- 每次通过
int get(int key)
接口查询到LRUCache中的一个元素,就将该元素在双向链表中移动到头部。 - 操作LRUCache的
void put(int key, int value)
接口可能会使得双向链表的节点数增加(当缓存中没有该key值,且缓存容量未满时),每次put操作(无论是存在key,只是更新value,还是不存在key,插入新的key-value对)都会将该put操作的key-value元素在双向链表中移动到头部 - 通过上述操作,双向链表的尾节点一定是LRUCache中最久没有被使用过的元素,则当双向链表超出限定长度后,删除超长的尾节点
要实现以上的双向链表操作,需要自己定义双向链表节点和相应的节点移动操作,在C++中可以通过自定义一个结构体或类来实现,其成员属性如下:
key
,value
是双向链表节点存的LRUCache元素(key-value对)prev
是指向前一个链表节点的前向指针next
是指向后一个链表节点的后向指针
操作双向链表的方法在LRUCache类中实现(因为需要靠LRUCache的初始化来定义双向链表的初始头尾哑节点)。这些函数功能如下,注意,我们使用了常用的添加头尾哑节点技巧来简化链表边界问题:
void addToHead(DLinkedNode *node)
给定一个新节点,将其添加到双向链表头部void removeNode(DLinkedNode *node)
给定一个链表中的节点,将其移除void moveToHead(DLinkedNode *node)
给定一个链表中的节点,将其移动到双向链表头部(显然此方法只需先调用removeNode()
再调用addToHead()
即可)DLinkedNode * removeTail()
移除双向链表中的尾节点,并返回指向该被移除节点的指针
综上,LRUCache的类设计图如下:
3 代码实现(C++)
直接上代码(附详细注释)
//LRUCache.cpp
#include <unordered_map>
using namespace std;
//实现双向链表节点
struct DLinkedNode {
//成员属性
int key, value;
DLinkedNode *prev;
DLinkedNode *next;
//构造函数
DLinkedNode() : key(0), value(0), prev(nullptr), next(nullptr) {}
DLinkedNode(int _key, int _value) : key(_key), value(_value), prev(nullptr), next(nullptr) {}
};
class LRUCache {
private:
unordered_map<int, DLinkedNode *> cache;
DLinkedNode *head;
DLinkedNode *tail;
int size; //LRU缓存的当前尺寸
int capacity; //LRU缓存的容量(初始化大小)
//实现双向链表操作的方法
void addToHead(DLinkedNode *node) { //将一个双向列表节点添加到链表头部
//请注意以下的head是指真正的头节点之前的哑节点
node->prev = head;
node->next = head->next;
head->next->prev = node;
head->next = node;
}
void removeNode(DLinkedNode *node) { //将一个双向链表节点从链表中移除
node->prev->next = node->next;
node->next->prev = node->prev;
}
void moveToHead(DLinkedNode *node) { //将双向链表节点移动到链表头部
removeNode(node);
addToHead(node);
}
DLinkedNode *removeTail() { //移除双向链表的尾部节点,返回其前一个节点(作为新的尾部节点)
//请注意以下的tail是指真正的尾节点之后的哑节点
DLinkedNode *node = tail->prev;
removeNode(node);
return node;
}
public:
LRUCache(int _capacity) : capacity(_capacity), size(0) {
//使用伪头部和伪尾部节点(哑节点)标记界限,这样在添加和删除节点时就无需检查相邻节点是否存在
head = new DLinkedNode();
tail = new DLinkedNode();
head->next = tail;
tail->next = head;
//仅加入头尾哑节点的初始化后,size=0,capacity=2
}
int get(int key) {
if (!cache.count(key)) { //key不存在,返回-1
return -1;
}
//如果key存在,先通过哈希表定位,再移动到链表头部
DLinkedNode *node = cache[key];
moveToHead(node);
return node->value;
}
void put(int key, int value) {
if (!cache.count(key)) { //如果key不存在
DLinkedNode *node = new DLinkedNode(key, value); //创建一个新的节点
cache[key] = node; //将其添加进哈希表
addToHead(node); //将其添加进双向链表头部
++size; //LRU缓存的size增长(注意capacity是初始化好的,不变)
if (size > capacity) { //如果存储量超出了LRU缓存的容量
DLinkedNode *removed = removeTail(); //删除双向链表的尾部节点
cache.erase(removed->key); //删除哈希表中的对应项
delete removed; //手动delete掉removed指针,防止内存泄漏
--size;
}
} else { //如果key存在
DLinkedNode *node = cache[key]; //先通过哈希表定位
node->value = value; //修改value
moveToHead(node); //将其在双向链表中移动到头部
}
}
};