题目解释
设计并实现最不经常使用(LFU)
缓存的数据结构。它应该支持以下操作:get
和 put
。
get(key)
- 如果键存在于缓存中,则获取键的值(总是正数),否则返回 -1。
put(key, value)
- 如果键不存在,请设置或插入值。当缓存达到其容量时,它应该在插入新项目之前,使最不经常使用的项目无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,最近最少使用的键将被去除。
示例
LFUCache cache = new LFUCache( 2 /* capacity (缓存容量) */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 去除 key 2
cache.get(2); // 返回 -1 (未找到key 2)
cache.get(3); // 返回 3
cache.put(4, 4); // 去除 key 1
cache.get(1); // 返回 -1 (未找到 key 1)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
解题思路
基本数据结构
- 字典,键为
key
,值为key
对应链表中的节点node
,即key:node
- 双向链表,自定义一个双向链表节点
dlNode
类,其中
- 链表数据为一个列表,包括
[key, value, cnt]
,其中key
和value
分别为缓存中key
和value
,cnt
用于记录被访问的次数,默认cnt=0
- 链表指针包括前指针
pre
后指针next
,即双向链表 - 链表中节点顺序是按照访问次数的降序排列,即访问次数越大,越靠前、访问次数相同情况下越“新”越靠前
主要方法逻辑:
__init__
: 定义容量self.c = capacity
,初始化字典self.cache = {}
,初始化双向链表。这里,在双向链表初始化中,为避免处理头、尾节点时的复杂特判,采用链表通用技巧即增加虚拟节点处理,因为是双向链表,所以加虚拟头尾两个节点并首尾相连self.head = dlNode(1, 1, float('inf'))
# 头节点,定义访问次数正无穷self.tail = dlNode(-1, -1, float('-inf'))
# 尾节点,定义访问次数负无穷self.head.next = self.tail
self.tail.pre = self.head
-refresh
: 辅助刷新位置函数,接收一个节点和对应新访问次数作为参数。更新原则是:- 如果节点访问次数小于其前节点的访问次数,说明仍然有序。无需更新
- 否则,根据
cnt
大小尽可能移动到靠前的位置,即移动到所有不大于其访问次数的节点之前,保证越大越“新”越靠前
- get:get操作意味着其访问次数+1,其实现逻辑:
如果缓存容量self.c <= 0或目标键值不在缓存字典中,直接返回-1
否则,通过字典找到该节点,通过节点找到其访问次数,访问次数+1后刷新其位置
最后,返回其value值 - put:put操作情况略显复杂,包括以下情况:
- 如果缓存容量self.c <= 0,直接返回
- 否则,区分待添加值是否已在缓存字典中:
- 如果已在缓存字典中,则仅简单更新访问次数并刷新位置即可(无需考虑缓存容量),需注意的是这里既要更新访问次数cnt,也要更新可能变化的value值
- 如果不在缓存字典,则加入之前还要考虑缓存容量是否已满:
- 如果缓存容量已满,则先剔除链表尾部节点(更 准确的说是self.tail前的那个节点)
- 加入节点,并前移到尽可能靠前的位置(调用_refresh())
代码实现
class dlNode:
def __init__(self, key, val, cnt=0):
self.val = [key, val, cnt]#键、值、访问次数
self.pre = None
self.nxt = None
class LFUCache:
def __init__(self, capacity: int):
self.cache = {}#通过key保存链表节点,key:node
self.c = capacity#字典容量
self.head = dlNode(1, 1, float('inf'))#头节点,定义访问次数正无穷
self.tail = dlNode(-1, -1, float('-inf'))#尾节点,定义访问次数负无穷
self.head.nxt = self.tail
self.tail.pre = self.head
def _refresh(self, node, cnt):##辅助函数,对节点node,以访问次数cnt重新定义其位置
pNode, nNode = node.pre, node.nxt #当前节点的前后节点
if cnt < pNode.val[2]:#如果访问次数小于前节点的访问次数,无需更新位置
return
pNode.nxt, nNode.pre = nNode, pNode#将前后连起来,跳过node位置
while cnt >= pNode.val[2]:#前移到尽可能靠前的位置后插入
pNode = pNode.pre
nNode = pNode.nxt
pNode.nxt = nNode.pre = node
node.pre, node.nxt = pNode, nNode
def get(self, key: int) -> int:
if self.c <= 0 or key not in self.cache:#如果容量<=0或者key不在字典中,直接返回-1
return -1
node = self.cache[key]#通过字典找到节点
_, value, cnt = node.val#通过节点得到key,value和cnt
node.val[2] = cnt+1#访问次数+1
self._refresh(node, cnt+1)#刷新位置
return value
def put(self, key: int, value: int) -> None:
if self.c <= 0:#缓存容量<=0
return
if key in self.cache:#已在字典中,则要更新其value,同时访问次数+1刷新位置
node = self.cache[key]
_, _, cnt = node.val
node.val = [key, value, cnt+1]#更新其值
self._refresh(node, cnt+1)
else:
if len(self.cache) >= self.c: #容量已满,先清除掉尾部元素
tp, tpp = self.tail.pre, self.tail.pre.pre
self.cache.pop(tp.val[0]) #从字典剔除尾节点
tpp.nxt, self.tail.pre = self.tail, tpp #首尾相连,跳过原尾节点
#新建节点,并先插入到队尾
node = dlNode(key, value)
node.pre, node.nxt = self.tail.pre, self.tail
self.tail.pre.nxt, self.tail.pre = node, node
self.cache[key] = node
self._refresh(node, 0)
# Your LFUCache object will be instantiated and called as such:
# obj = LFUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)