在计算机科学中,缓存是提高性能的关键技术之一。而当缓存空间有限时,我们就需要一种策略来决定哪些数据应该保留,哪些应该被淘汰。LRU(Least
Recently
Used,最近最少使用)就是其中一种非常经典且广泛使用的缓存淘汰算法。它的核心思想是:如果数据最近被访问过,那么将来被访问的几率也更高。
因此,当缓存满了需要淘汰数据时,应该优先淘汰那些最长时间未被访问过的数据。
LRU 算法的核心需求
一个合格的 LRU 缓存需要高效地支持以下操作:
get(key)
:通过键 key
获取对应的值
value
。如果键存在,则返回其值,并将该数据标记为“最近使用”;如果键不存在,则返回一个表示未命中的值(如
-1 或 null)。
put(key, value)
:插入或更新一个键值对。
- 如果
key
已存在,更新其
value
,并将该数据标记为“最近使用”。
- 如果
key
不存在:
- 检查缓存是否已满。
- 如果未满,直接插入新的键值对,并将其标记为“最近使用”。
- 如果已满,则先淘汰掉“最久未使用”的数据,然后再插入新的键值对,并将其标记为“最近使用”。
- 高效性要求: 上述
get
和
put
操作的时间复杂度最好能达到 O(1)。
数据结构的选择:哈希表 +
双向链表
为了同时满足 O(1)
的查找、插入、删除和移动(标记为最近使用)要求,单一的数据结构很难胜任。LRU
的经典实现巧妙地结合了两种数据结构:
- 哈希表(
std::unordered_map
):
用于存储 key
到链表节点的映射。这使得我们能够以 O(1)
的平均时间复杂度快速查找一个 key
是否存在于缓存中,并直接定位到它在链表中的位置。
- 双向链表(
std::list
):
用于维护数据的“最近使用”顺序。
- 链表的头部(Head)存放最近使用的数据。
- 链表的尾部(Tail)存放最久未使用的数据。
- 当一个数据被访问(
get
或
put
),我们就将其对应的链表节点移动到链表头部。这需要 O(1)
时间。
- 当需要淘汰数据时,我们直接删除链表尾部的节点。这需要 O(1)
时间。
- 插入新数据时,将其插入到链表头部。这需要 O(1) 时间。
实现细节
让我们用 C++ 来勾勒一下实现框架:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
| #include <iostream> #include <unordered_map> #include <list>
class LRUCache { private: struct CacheNode { int key; int value; };
int capacity; std::list<CacheNode> cacheList; std::unordered_map<int, std::list<CacheNode>::iterator> cacheMap;
void moveToFront(int key, std::list<CacheNode>::iterator it) { CacheNode node = *it; cacheList.erase(it); cacheList.push_front(node); cacheMap[key] = cacheList.begin(); }
public: LRUCache(int cap) : capacity(cap) {}
int get(int key) { auto map_it = cacheMap.find(key); if (map_it == cacheMap.end()) { return -1; }
std::list<CacheNode>::iterator list_it = map_it->second;
int value = list_it->value; moveToFront(key, list_it);
return value; }
void put(int key, int value) { auto map_it = cacheMap.find(key); if (map_it != cacheMap.end()) { std::list<CacheNode>::iterator list_it = map_it->second; list_it->value = value; moveToFront(key, list_it); } else {
if (cacheMap.size() >= capacity) { int keyToRemove = cacheList.back().key; cacheMap.erase(keyToRemove); cacheList.pop_back(); }
cacheList.push_front({key, value}); cacheMap[key] = cacheList.begin(); } }
void printCache() { std::cout << "Cache Content (Front=Recent, Back=Old): "; for (const auto& node : cacheList) { std::cout << "[" << node.key << ":" << node.value << "] "; } std::cout << std::endl; std::cout << "Map Size: " << cacheMap.size() << ", List Size: " << cacheList.size() << std::endl; } };
int main() { LRUCache cache(2);
cache.put(1, 1); cache.printCache(); cache.put(2, 2); cache.printCache(); std::cout << "Get(1): " << cache.get(1) << std::endl; cache.printCache(); cache.put(3, 3); cache.printCache(); std::cout << "Get(2): " << cache.get(2) << std::endl; cache.printCache(); cache.put(4, 4); cache.printCache(); std::cout << "Get(1): " << cache.get(1) << std::endl; std::cout << "Get(3): " << cache.get(3) << std::endl; cache.printCache(); std::cout << "Get(4): " << cache.get(4) << std::endl; cache.printCache();
return 0; }
|
复杂度分析
- 时间复杂度:
get(key)
:哈希表查找是 O(1) 平均,链表移动是
O(1)。总平均时间复杂度 O(1)。
put(key, value)
:哈希表查找/插入是 O(1)
平均,链表插入/删除/移动是 O(1)。总平均时间复杂度 O(1)。
- 空间复杂度: 哈希表和双向链表都需要存储最多
capacity
个元素(键和值,以及链表指针/迭代器)。所以空间复杂度是
O(capacity)。
总结
LRU
缓存算法通过结合哈希表(快速查找)和双向链表(快速排序和淘汰)的优点,实现了高效的缓存管理。
LeetCode
参考题目 146. LRU 缓存