← 返回题目列表

LFU 缓存如何用哈希表做到 O(1)?

高频 困难 第 19 / 29 题 更新于 2026/07/29
哈希表LFU缓存设计

简化版

LFU 按访问频次淘汰,频次相同再淘汰最久未使用。O(1) 设计常用两层哈希:key -> 节点 快速定位,freq -> 双向链表 管理同频节点的 LRU 顺序,再维护 minFreq 找到最低频次。

详细版

get 命中后要把节点频次加 1,并从旧频次链表移动到新频次链表头部;put 超容量时,从 minFreq 对应链表尾部删除最旧节点。哈希表负责 O(1) 定位,双向链表负责 O(1) 删除和插入。

keyMap:  key -> node(key,val,freq)
freqMap: freq -> linked list of nodes, head = newest
minFreq: 当前最小频次

LFU 比 LRU 多了频次维度,所以不能只用一个链表。

完整版教学

一、LFU 和 LRU 的差别

LRU 看“最近有没有用”,LFU 看“总共用得频不频繁”。如果缓存满了,LFU 要淘汰访问次数最低的 key;若最低频次有多个,再按 LRU 淘汰其中最久未访问的。

例如容量 2,操作 put(1), put(2), get(1), put(3):key1 频次 2,key2 频次 1,所以淘汰 key2。

二、为什么一个 HashMap 不够

一个 HashMap 能 O(1) 找到 key,但不知道哪个 key 频次最低、同频里谁最旧。若每次淘汰都扫描所有节点找最低频,put 会退化到 O(n)。

LFU 需要同时支持三种 O(1) 能力:

能力结构
按 key 找节点key -> node
按频次找一组节点freq -> list
同频内淘汰最旧双向链表尾部

这就是两层哈希加链表的来源。

三、节点里需要存什么

节点至少存 keyvaluefreq,以及链表前后指针。存 key 是为了淘汰时能从 keyMap 中删除对应项;存 freq 是为了 get 时知道它当前在哪个频次链表。

class Node {
    int key, val, freq = 1;
    Node prev, next;
}

如果不在节点里存 freq,每次提升频次都得反查它所在链表,O(1) 设计会变复杂。

四、get 命中时发生什么

get(key) 命中后,不只是返回 value,还要提升频次。步骤是:从旧频次链表删除节点;如果旧频次等于 minFreq 且链表空了,minFreq++;把节点 freq 加 1;插入新频次链表头部。

freq=1: [A, B]
get(B)
freq=1: [A]
freq=2: [B]

插入头部表示 B 在 freq=2 这一组里最近被访问。

五、put 超容量时怎么淘汰

新 key 插入时频次为 1,所以如果缓存超过容量,要先淘汰 minFreq 链表尾部的节点。尾部代表该频次下最久未使用的节点,满足“频次最低 + 同频 LRU”。

minFreq = 1
freq=1 链表: head [newer ... older] tail
淘汰 tail.prev

插入新节点后,minFreq 应设为 1,因为新节点频次就是 1。

六、常见误区与追问

记忆钩子:LFU 是“先按频次分桶,再在桶内按最近使用排序”。

  • 误区:LFU 只要记录访问次数即可。 淘汰时还要在最低频次中找最旧节点,需要链表维护顺序。
  • 误区:一个全局链表能解决。 全局链表只能表达最近使用顺序,不能快速定位最低频次组。
  • 误区:get 不改变缓存状态。 LFU 中 get 会增加频次并移动节点。
  • 追问:capacity 为 0 怎么办? put 直接返回,不能插入任何节点。
  • 追问:为什么要 minFreq 避免淘汰时扫描所有频次,保持 O(1)。
  • 追问:LFU 的缺点是什么? 老热点可能因历史频次过高长期不淘汰,工程中常配合衰减或窗口策略。

七、加强记忆

LFU 的 O(1) 设计要背结构关系:keyMap 管定位,freqMap 管频次桶,桶内双向链表管同频 LRU,minFreq 管最低频次入口。每次 get 是“从旧桶搬到新桶”,每次淘汰是“删 minFreq 桶尾”。这比 LRU 多的正是频次分桶这一层。