题目重点:用key映射和频率桶维护平均O(1)读写,明确定义更新计数与零容量,不把TTL混入基础LFU。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 训练get命中与已有key的put均将频率加1,新key频率1;缺失get返回None,存值避免None歧义。
- 淘汰最低频率,同频淘汰最久未被touch者;capacity非负,0不存。
- 无TTL、显式delete、容量调整或并发要求;这些操作会改变最小频率维护逻辑。
解题路线
- 哈希表保存value与frequency,另一个映射把每个频率对应到按最近使用排序的OrderedDict桶。每个key恰好位于自己频率桶一次,最旧在桶首。
- touch时从频率f桶删除key,将频率改为f+1并放到新桶尾;这是同频的最新访问。空桶删除,避免长期累积空频率结构。
- minimum_frequency记录当前最小非空桶。touch若搬空最小桶,最小值可以直接加1,因为刚被搬走的key必在f+1形成非空桶,且之前没有低于f的key。这个论证依赖只有+1增长。
- 插新key且满容量时,从最小桶首popitem(last=False)淘汰,删value/frequency映射;新key进频率1,所以minimum_frequency必须重置1,不能沿用旧高频值。
- 已有key的put按训练视为一次访问,改值并touch,不需要腾位置;若正式更新不计频率,操作应另实现并明确是否刷新同频LRU位置。
- 正确性由桶成员与frequency一致、桶顺序按touch时间、minimum定位最小非空桶三条不变量保证。淘汰两级排序等价于先按频率再按时间扫描,但不需要每次遍历全部key。
- LFU保留长期热门,热点变迁后旧高频key可能难被替换。实际系统可用衰减/近似计数或窗口策略,但这些已偏离本训练精确LFU;频率整数增长的位数和长期行为也应说明。
- TTL或任意删除可能一次删掉最低桶且没有f+1元素,不能简单minimum+=1。并发下映射与桶必须原子更新,可锁保护或分片并接受局部策略。
手动走一遍
容量2:put(a,1),put(b,2),get(a)后a频率2、b频率1,put(c,3)淘汰b。随后get(c)使a/c同频2,若a更早touch,put(d,4)淘汰a。更新已有c也会增频,不当作新插入。
Python 参考实现
下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。
代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。
from collections import OrderedDict, defaultdict
class LFUCache:
"""Least frequency first, LRU among keys with equal frequency."""
def __init__(self, capacity: int):
if capacity < 0:
raise ValueError("capacity must be nonnegative")
self.capacity = capacity
self.values = {}
self.frequency = {}
self.buckets = defaultdict(OrderedDict)
self.minimum_frequency = 0
def _touch(self, key) -> None:
old_frequency = self.frequency[key]
del self.buckets[old_frequency][key]
if not self.buckets[old_frequency]:
del self.buckets[old_frequency]
if self.minimum_frequency == old_frequency:
self.minimum_frequency += 1
new_frequency = old_frequency + 1
self.frequency[key] = new_frequency
self.buckets[new_frequency][key] = None
def get(self, key):
if key not in self.values:
return None
self._touch(key)
return self.values[key]
def put(self, key, value) -> None:
if self.capacity == 0:
return
if key in self.values:
self.values[key] = value
self._touch(key)
return
if len(self.values) == self.capacity:
oldest_key, _ = self.buckets[self.minimum_frequency].popitem(last=False)
del self.values[oldest_key]
del self.frequency[oldest_key]
if not self.buckets[self.minimum_frequency]:
del self.buckets[self.minimum_frequency]
self.values[key] = value
self.frequency[key] = 1
self.buckets[1][key] = None
self.minimum_frequency = 1
复杂度
在哈希和OrderedDict操作平均O(1)假设下get/put平均O(1),非空桶和key共O(capacity)结构空间。频率数值随访问次数增长,其大整数位数另计;未承诺严格最坏O(1)。
易错点与追问
容易失分的边界
- 同频随机淘汰而合同要求LRU。
- 新key未把minimum设1。
- 搬空最小桶不更新minimum。
- 任意delete仍套最低频率加1。
继续深挖
- TTL与delete怎样找下一个非空频率?
- 热点迁移后如何衰减计数?
- 更新值不计访问怎样改?
- 多线程或分片如何保持桶不变量?
