
常见缓存淘汰策略
-
FIFO(First In First Out)先进先出策略会将数据按照写入缓存的顺序进行排队,当缓存空间不足时,最先进入缓存的数据会被优先删除。是一种比较死板的策略不考虑数据热度可能会淘汰大量的热点数据,但是实现起来相对容易。
-
LRU(Least Recently Used)是最经典的内存淘汰策略,其设计原则是"如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小"。即根据数据的最近访问时间来进行淘汰,缺点是可能会由于一次冷数据的批量查询而误删除大量的热点数据
-
TTL(Time ToLive)是指用户为缓存设置的过期时间,当前时间到达过期时间时将删除缓存;如果缓存空间已满,则优先淘汰最接近过期时间的数据。
-
LFU(Least Frequently Used)策略会记录每个缓存数据的最近访问次数(频率),并优先清除使用次数较少的数据。这种算法存在的显著缺点是,最新写入的数据由于访问次数少,常常刚被缓存就删除
-
Random 随机淘汰策略,一般不建议使用。
- 题目要求 get 和 put 都要在 O ( 1 ) O(1) O(1) 时间内完成
- 要将两种完全不同的数据结构结合起来:哈希表(Hash Table) 和 双向链表(Doubly Linked List)。
- 哈希表:查询快。给定一个 key,能瞬间找到它对应的 value。但它没有顺序,无法知道谁是"最近使用"的。
- 链表:有顺序。我们可以约定:靠近头部的节点是"最近使用的",靠近尾部的是"最久未使用的"。移动节点非常快。
- 双向链表:要把某个中间节点删除并移动到头部,只有双向链表能在 O ( 1 ) O(1) O(1) 内拿到它的前驱节点。
LRU规则
- 访问即更新:无论是 get(key) 还是 put(key, val),只要这个 key 被碰过了,它就变成了"最受宠"的,必须移动到链表的最前面(头)。
- 满员则淘汰:当缓存满了还要 put 新东西时,直接把链表**最后面(尾)**的那个节点踢掉,同时在哈希表里也删掉它。
示例动态拆解
比如一个果篮大小为3 apple orange banana依次进入 {banana,orange,apple}
你想拿去苹果看一眼再放回果篮 {apple,banana,orange}
现在来了新的水果peach超出果篮大小需要丢掉一个水果 {peach,apple,bnana}
可以得出规律 新来的放最前面 刚访问过的移动到最前面 空间满了就把最旧的淘汰掉
python
LRUCache lRUCache = new LRUCache(2); // 假设容量为 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
python collections.OrderedDict 内部其实维护了一个哈希表 + 双向链表
它不仅简洁,而且完全符合 O ( 1 ) O(1) O(1) 的时间复杂度要求。
在 OrderedDict 的 LRU 实现中,最常见的做法是:
- 末尾 (End/Right):定义为 最新使用 的位置。
- 开头 (Head/Left):定义为 最久未使用 的位置。
OrderedDict popitem() 方法
- last=True (默认值):按照 "后进先出" (LIFO) 原则。弹出最后面(最右边)插入的元素。这就像从一叠盘子的最上面拿走一个。
- last=False:按照 "先进先出" (FIFO) 原则。弹出最前面(最左边)插入的元素。这正是 LRU 需要的------踢掉那个最早进来且一直没被更新过的人。
时间复杂度:get 和 put 都是 O ( 1 ) O(1) O(1)。哈希表查找 O ( 1 ) O(1) O(1),双向链表断开和连接指针也是 O ( 1 ) O(1) O(1)。
空间复杂度: O ( n ) O(n) O(n)。哈希表和双向链表最多各存储 n 个元素。
python
# class Node:
# def __init__(self):
# # 实现键值和前驱与后继
# self.key = key
# self.value = value
# self.prev = None
# self.next = None
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int): # 变量名capacity
self.capacity = capacity # 传入参数capacity 初始化, capacity字典容量
self.cache = OrderedDict() # 库函数OrderedDict 初始化, 新建字典
# 取值
def get(self, key: int) -> int:
# 没有返回-1
if key not in self.cache:
return -1
# 如果有移动到最新并返回值
self.cache.move_to_end(key) # 在 OrderedDict 的 LRU 实现中:末尾 (End/Right) 定义为 最新使用 的位置。
return self.cache[key]
# 存值
def put(self, key: int, value: int) -> None:
# # 如果值已经存在 移动该值到最新
if key in self.cache:
self.cache.move_to_end(key)
self.cache(key) = value # key的值可能会更新,重新赋值
# 弹出最旧 最先放进去的那个
if len(self.cache) > self.capacity: # 大于字典容量
self.cache.popitem(last=False) # last=False 弹出最左边的(最早的)
# Your LRUCache object will be instantiated and called as such:
# obj = LRUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)