C++手撕LRU
背景
小H最近在准备面试,发现手撕的环节除了一些算法部分还有偏工程部分,而其中手撕LRU又是比较常见的,所以决定整理一下。
LRU
LRU(Least Recently Used,最近最少使用) 是一种常见的缓存淘汰策略,用于在缓存空间不足时,决定哪些数据应该被移除,以腾出空间存储新数据。其核心思想是:如果一条数据最近很少被访问,那么未来被访问的概率也很低,因此优先淘汰这类数据。
核心原理
当缓存达到最大容量时,LRU 策略会筛选出最近一次访问时间最早(即最久未被使用) 的数据,将其从缓存中删除,然后存入新数据。
举例来说,假设缓存容量为 3,数据访问顺序如下:
- 访问 A → 缓存:[A](最近使用:A)
- 访问 B → 缓存:[A, B](最近使用:B)
- 访问 C → 缓存:[A, B, C](最近使用:C)
- 访问 B → 缓存:[A, C, B](最近使用:B,B 被重新访问,位置更新)
- 访问 D(缓存满)→ 淘汰最久未用的 A → 缓存:[C, B, D](最近使用:D)
实现方式
LRU 缓存需要高效支持两种操作:
- 访问数据(get):若数据在缓存中,需将其标记为 “最近使用”;若不在,返回未命中。
- 插入数据(put):若缓存未满,直接插入并标记为 “最近使用”;若已满,先删除最久未用数据,再插入新数据。
常见的实现结构是 “哈希表 + 双向链表”:
- 双向链表:按访问时间排序,头部存放最近使用(MRU)的数据,尾部存放最久未用(LRU)的数据。
- 哈希表:键为数据的键,值为双向链表中对应节点的指针,用于 O (1) 时间复杂度定位数据。
这种组合可使 get
和 put
操作的时间复杂度均为 O (1),本次双向链表我们也是自己实现,没有直接使用List,哈希表就直接使用unordered_map就行了。
struct ListNode {int key;int value;ListNode* prev;ListNode* next;ListNode(int k,int v):key(k),value(v),prev(nullptr),next(nullptr){}
};class DoublyLinkedList{
public:ListNode* head;ListNode* tail;DoublyLinkedList(){head = new ListNode(0,0);tail = new ListNode(0,0);head->next = tail;tail->prev = head;}~DoublyLinkedList(){clear();delete head;delete tail;}// 清空链表void clear(){ListNode* now = head->next;while(now != tail){ListNode* nextNode = now->next;delete now;now = nextNode;}head->next = tail;tail->prev = head;}void push_front(ListNode* node){node->next = head->next;node->prev = head;head->next->prev = node;head->next = node;}void erase(ListNode* node){node->prev->next = node->next;node->next->prev = node->prev;}void move_to_front(ListNode* node){node->prev->next = node->next;node->next->prev = node->prev;push_front(node);}ListNode* pop_back() {ListNode *node = tail->prev;if (node == head) return nullptr;// 断开连接但是不删除节点node->prev->next = tail;tail->prev = node->prev;node->prev = nullptr;node->next = nullptr;return node;}
};class LRUCache{
public:LRUCache(int capacity):_capacity(capacity){}int get(int key){auto it=_cache.find(key);if(it == _cache.end()){return -1;}_list.move_to_front(it->second);return it->second->value;}void put(int key,int value){auto it=_cache.find(key);if(it != _cache.end()){it->second->value=value;_list.move_to_front(it->second);}else{if(_cache.size()==_capacity){ListNode* node = _list.pop_back();if(node!= nullptr){_cache.erase(node->key);delete node;}}ListNode* newNode = new ListNode(key,value);_list.push_front(newNode);_cache[key] = newNode;}}private:int _capacity;DoublyLinkedList _list;std::unordered_map<int,ListNode*> _cache;
};
代码上没有太复杂的地方,主要就是关注一下释放节点的时候前后指针如何变换、还有满载、访问的策略。
附上一份测试代码。
void test()
{LRUCache cache(2);cache.put(1, 1); // 缓存现在为 {1=1}cache.put(2, 2); // 缓存现在为 {1=1, 2=2}std::cout << "Get 1: " << cache.get(1) << std::endl; // 返回 1cache.put(3, 3); // 缓存达到容量,移除最近最少使用的键 2,缓存现在为 {1=1, 3=3}std::cout << "Get 2: " << cache.get(2) << std::endl; // 返回 -1(未找到)cache.put(4, 4); // 缓存达到容量,移除最近最少使用的键 1,缓存现在为 {3=3, 4=4}std::cout << "Get 1: " << cache.get(1) << std::endl; // 返回 -1(未找到)std::cout << "Get 3: " << cache.get(3) << std::endl; // 返回 3std::cout << "Get 4: " << cache.get(4) << std::endl; // 返回 4
}
结语
如果本章内容上有什么问题,可以与作者联系。