460. LFU 缓存 #
链接 #
题目 #
请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。
实现 LFUCache
类:
LFUCache(int capacity)
- 用数据结构的容量capacity
初始化对象int get(int key)
- 如果键key
存在于缓存中,则获取键的值,否则返回-1
。void put(int key, int value)
- 如果键key
已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量capacity
时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最久未使用 的键。
为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。
当一个键首次插入到缓存中时,它的使用计数器被设置为 1
(由于 put 操作)。对缓存中的键执行 get
或 put
操作,使用计数器的值将会递增。
函数 get
和 put
必须以 O(1)
的平均时间复杂度运行。
示例:
输入: ["LFUCache", "put", "put", "get", "put", "get", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]] 输出: [null, null, null, 1, null, -1, 3, null, -1, 3, 4] 解释: // cnt(x) = 键 x 的使用计数 // cache=[] 将显示最后一次使用的顺序(最左边的元素是最近的) LFUCache lfu = new LFUCache(2); lfu.put(1, 1); // cache=[1,_], cnt(1)=1 lfu.put(2, 2); // cache=[2,1], cnt(2)=1, cnt(1)=1 lfu.get(1); // 返回 1 // cache=[1,2], cnt(2)=1, cnt(1)=2 lfu.put(3, 3); // 去除键 2 ,因为 cnt(2)=1 ,使用计数最小 // cache=[3,1], cnt(3)=1, cnt(1)=2 lfu.get(2); // 返回 -1(未找到) lfu.get(3); // 返回 3 // cache=[3,1], cnt(3)=2, cnt(1)=2 lfu.put(4, 4); // 去除键 1 ,1 和 3 的 cnt 相同,但 1 最久未使用 // cache=[4,3], cnt(4)=1, cnt(3)=2 lfu.get(1); // 返回 -1(未找到) lfu.get(3); // 返回 3 // cache=[3,4], cnt(4)=1, cnt(3)=3 lfu.get(4); // 返回 4 // cache=[3,4], cnt(4)=2, cnt(3)=3
提示:
1 <= capacity <= 104
0 <= key <= 105
0 <= value <= 109
- 最多调用
2 * 105
次get
和put
方法
解答 #
最不经常使用(Least Frequently Used, LFU)缓存算法是一种缓存淘汰算法,它会根据数据项被访问的频率来进行淘汰数据,频率最低的数据项最先被淘汰。实现一个高效的 LFU 缓存机制需要考虑到时间复杂度,特别是在查找、插入和删除操作上。
下面是一个使用 Python 实现的 LFU 缓存的示例,这个实现利用了双哈希表和双向链表来确保所有操作的时间复杂度尽可能低:
- 第一个哈希表用于存储键到缓存节点的映射,以实现 (O(1)) 时间复杂度的查找。
- 第二个哈希表用于存储频率到包含具有该频率的所有节点的双向链表的映射,以实现 (O(1)) 时间复杂度的插入和删除。
- 每个频率对应的节点组织在一个双向链表中,以便于在相同频率的节点中保持插入顺序,从而实现 (O(1)) 时间复杂度的最小频率节点的查找和删除。
class Node:
def __init__(self, key, value, freq=1):
self.key = key
self.value = value
self.freq = freq
self.prev = self.next = None
class DLinkedList:
def __init__(self):
self.sentinel = Node(None, None)
self.sentinel.next = self.sentinel.prev = self.sentinel
self.size = 0
def append(self, node):
node.next = self.sentinel.next
node.prev = self.sentinel
self.sentinel.next.prev = node
self.sentinel.next = node
self.size += 1
def remove(self, node):
node.prev.next = node.next
node.next.prev = node.prev
self.size -= 1
def pop(self):
if self.size == 0:
return
node = self.sentinel.prev
self.remove(node)
return node
class LFUCache:
def __init__(self, capacity):
self.capacity = capacity
self.minFreq = 0
self.keyTable = {}
self.freqTable = {}
def _update(self, node):
freq = node.freq
self.freqTable[freq].remove(node)
if self.minFreq == freq and not self.freqTable[freq].size:
self.minFreq += 1
node.freq += 1
freq = node.freq
if freq not in self.freqTable:
self.freqTable[freq] = DLinkedList()
self.freqTable[freq].append(node)
def get(self, key):
if key not in self.keyTable:
return -1
node = self.keyTable[key]
self._update(node)
return node.value
def put(self, key, value):
if self.capacity == 0:
return
if key in self.keyTable:
node = self.keyTable[key]
node.value = value
self._update(node)
else:
if len(self.keyTable) == self.capacity:
minFreqList = self.freqTable[self.minFreq]
delNode = minFreqList.pop()
del self.keyTable[delNode.key]
newNode = Node(key, value)
self.keyTable[key] = newNode
if 1 not in self.freqTable:
self.freqTable[1] = DLinkedList()
self.freqTable[1].append(newNode)
self.minFreq = 1
# 使用示例
lfu = LFUCache(2)
lfu.put(1, 1)
lfu.put(2, 2)
print(lfu.get(1)) # 返回 1
lfu.put(3, 3) # 移除 key 2
print(lfu.get(2)) # 返回 -1 (未找到)
print(lfu.get(3)) # 返回 3
这个实现确保了 get
和 put
操作都具有 (O(1)) 的时间复杂度,满足了复杂度最优的要求。