← 返回题目列表

LRU 缓存为什么常用哈希表加双向链表实现?

高频 困难 第 18 / 28 题 更新于 2026/07/29
链表双向链表LRU哈希表

简化版

LRU 需要 O(1) 查找、O(1) 删除任意节点、O(1) 移到最近使用位置。哈希表负责通过 key 找到节点,双向链表负责维护访问顺序;访问或更新就把节点移到头部,容量满了淘汰尾部节点。

详细版

单靠数组或单链表很难同时做到 O(1)。哈希表能 O(1) 找到 key 对应节点,但不能维护最近使用顺序;双向链表能 O(1) 删除已知节点并移动到头部,也能 O(1) 删除尾部最久未使用节点。

hash: key -> node
list: head <-> recent ... old <-> tail

面试要强调双向链表的价值:已知节点时,可以通过 prevnext 在 O(1) 时间把它摘掉;单链表没有前驱,删除任意节点不方便。

完整版教学

一、LRU 要维护最近使用顺序

LRU 是 Least Recently Used,淘汰最久没有被访问的数据。缓存容量固定时,每次 getput 都会改变某个 key 的“最近使用”状态。

目标操作:

get(key): 查到后变成最近使用
put(key,value): 新增或更新后变成最近使用
evict: 容量满时删除最久未使用

如果这些操作不能做到 O(1),缓存高频访问时就会成为瓶颈。

记忆钩子:哈希表管找到,双向链表管顺序。

二、哈希表解决 O(1) 查找

通过哈希表,可以从 key 快速找到节点。

map["A"] -> nodeA
map["B"] -> nodeB

如果没有哈希表,只靠链表查 key,需要从头扫到尾,时间 O(n)。缓存每次 get 都 O(n),容量一大就不可接受。

但哈希表本身没有“谁最近、谁最旧”的顺序信息,所以还需要链表。

三、双向链表维护使用顺序

常见约定是头部表示最近使用,尾部表示最久未使用。

head <-> A <-> C <-> B <-> tail
recent              old

访问 B 后,要把 B 移到头部:

head <-> B <-> A <-> C <-> tail

容量满时,淘汰 tail.prev,也就是最久未使用节点。这个操作 O(1)。

四、为什么必须是双向链表

如果哈希表给你 nodeB,想从链表里删除 B:

A <-> B <-> C

双向链表可以直接:

B.prev.next = B.next;
B.next.prev = B.prev;

单链表节点没有 prev。即使你拿到了 B,也不知道 A 是谁,删除 B 仍然麻烦。除非哈希表存前驱,但移动节点后前驱会频繁变化,维护复杂。

这就是 LRU 高频追问“为什么不是单链表”的答案。

五、dummy head 和 dummy tail 简化边界

LRU 实现常用虚拟头尾节点。

head <-> real nodes <-> tail

插入到头部:

node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;

删除节点:

node.prev.next = node.next;
node.next.prev = node.prev;

有 dummy 后,空链表、只有一个节点、删除尾部真实节点都不需要特殊分支。

六、get 和 put 的完整逻辑

get(key)

1. map 查不到 -> 返回 -1
2. map 查到 node -> 从链表摘除 node
3. node 插到头部
4. 返回 node.value

put(key,value)

1. key 已存在 -> 更新 value,移动到头部
2. key 不存在 -> 新建节点,插到头部,放入 map
3. 如果 size > capacity -> 删除 tail.prev,并从 map 删除 key

所有步骤都能做到 O(1),前提是哈希表和双向链表配合。

七、常见误区与追问

  • 误区:哈希表一个结构就能实现 LRU。 哈希表能查找,但不能维护最近使用顺序和 O(1) 淘汰最旧。
  • 误区:单链表也一样方便。 单链表删除已知节点缺少前驱,移动任意节点到头部不够方便。
  • 误区:只有 put 才算使用。 LRU 中 get 命中也要刷新最近使用状态。
  • 追问:为什么要 dummy head/tail? 统一插入删除边界,避免空链表和首尾节点特殊处理。
  • 追问:容量满淘汰谁? 淘汰尾部真实节点,也就是 tail.prev
  • 追问:时间复杂度为什么是 O(1)? map O(1) 定位节点,双向链表 O(1) 摘除和头插。

八、加强记忆

LRU 结构记成“map 找人,双链排队”。哈希表让 key 到节点 O(1),双向链表让已知节点 O(1) 删除、O(1) 移到头部、O(1) 淘汰尾部。get 命中和 put 更新都要刷新最近使用,容量满删 tail.prev,dummy 头尾负责把边界处理变简单。