链表——LRU缓存

题目要求:
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

要实现O (1) 平均时间复杂度的 LRU 缓存,核心思路是:

  1. 哈希表(HashMap):快速定位缓存中的 key,保证 get/put 查找 O (1)
  2. 双向链表:维护缓存的使用顺序,头部是最近使用的,尾部是最久未使用的,保证节点移动 / 删除 O (1)

核心逻辑说明

1. 数据结构设计

  • 双向链表节点:存储key/value,并包含前驱/后继指针
  • 虚拟头尾节点:避免操作链表时判断空指针,简化代码
  • HashMap:建立key -> 链表节点的映射,实现O(1)查找

2. 核心操作规则
1. get(key)

  • 若key不存在 -> 返回-1
  • 若key存在 -> 将对应节点移到链表头部标记未最近使用),返回值

2.put(key,value)

  • 若key已存在->更新节点值,并移到链表头部
  • 若key不存在->新建节点插入头部,若超过容量,删除链表尾部节点最久未使用

3.链表操作(均为O(1))

  • addToHead:将节点添加到头部(最近使用)
  • removeNode:删除指定节点
  • moveToHead:节点移动到头部(复用删除 + 添加)
  • removeTail:删除尾部最久未使用节点

1. cache.remove(tailNode.key); tailNode.key是什么意思?
为什么必须用 tailNode.key?

因为哈希表 cache 删除数据必须用 key
而我们刚刚删掉的是链表节点
哈希表不知道要删哪个 key
所以必须从节点里把 key 取出来,告诉哈希表删谁。

2. 双链表的作用?

HashMap 负责快速找 key(O (1) 查询)
双向链表 负责记录「使用顺序」+ O (1) 增删移动

一、为什么一定要用「双向链表」,不能用数组 / 单向链表?
1. LRU 的本质需求
需要动态维护顺序:

  • 刚get/put的数据 -> 最近使用,放最前面
  • 长时间没碰的数据 -> 最久未使用,放最后面
  • 容量超了 -> 删掉末尾最久没用的

并且所有顺序调整操作,必须O(1)

二、双向链表 三大核心作用

作用1:维护使用时序
链表规则:

  • 链表头部:最近刚使用的数据
  • 链表尾部:最久未使用的数据
    每次访问/修改数据:
    把对应节点挪到头部,自动更新时序
    链表天然适合维护这种动态先后顺序

作用2:实现O(1)删除任意节点

  • 单向链表:删中间节点需要从头遍历找前驱->O(n)
  • 双向链表:每个节点有prev前驱、next后继
    拿到任意节点,直接:
    node.prev.next = node.next;
    node.next.prev = node.prev;
    无需遍历,直接删,O(1)
    这是LRU 能过时间复杂度要求的关键。

作用3:O(1)快速淘汰尾部节点
缓存满了要驱逐最久未使用:
双向链表直接拿tail.prev就是最后一个有效节点,
删除尾部、淘汰数据、全程O(1)。

三、配合HashMap分工

1. HashMap

  • 根据 key → 快速找到链表节点
  • 解决:查得快 O (1)

2. 双向链表

  • 控制节点先后顺序、节点移动、头尾删除
  • 解决:顺序维护+增删改O(1)

四、补充:为什么不是单向链表?

假设用单向链表:要把一个中间节点移到头部时,
必须从头遍历找它的前一个节点,时间复杂度退化到 O(n),
不符合题目 get/put 必须 O (1) 的要求。
双向链表就是为了:快速删任意节点而生。

完整Java代码实现如下:
import java.util.HashMap;
import java.util.Map;

// LRU 缓存核心类
public class LRUCache {

// 双向链表节点类(存储 key 和 value)
static class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
// 空构造函数
public DLinkedNode() {}
// 带参构造函数
public DLinkedNode(int key, int value) {
this.key = key;
this.value = value;
}
}

// 哈希表:key -> 双向链表节点,O(1) 查找节点
private Map<Integer, DLinkedNode> cache;
// 缓存容量
private int capacity;
// 链表大小(当前缓存元素个数)
private int size;
// 虚拟头节点、虚拟尾节点(简化边界操作,无需判断 null)
private DLinkedNode head, tail;

// 初始化 LRU 缓存
public LRUCache(int capacity) {
this.capacity = capacity;
this.size = 0;
this.cache = new HashMap<>();
// 创建虚拟头尾节点
head = new DLinkedNode();
tail = new DLinkedNode();
// 初始化双向链表:head <-> tail
head.next = tail;
tail.prev = head;
}

// 获取缓存值:存在则返回值并更新为最近使用,否则返回 -1
public int get(int key) {
DLinkedNode node = cache.get(key);
// 缓存不存在
if (node == null) {
return -1;
}
// 缓存存在:将节点移到链表头部(标记为最近使用)
moveToHead(node);
return node.value;
}

// 插入/更新缓存
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 1. key 不存在:新建节点,插入头部
DLinkedNode newNode = new DLinkedNode(key, value);
cache.put(key, newNode);
addToHead(newNode);
size++;
// 超过容量:删除尾部最久未使用节点
if (size > capacity) {
DLinkedNode tailNode = removeTail();
cache.remove(tailNode.key);
size--;
}
} else {
// 2. key 已存在:更新值,并移到头部
node.value = value;
moveToHead(node);
}
}

// ---------------- 双向链表核心操作(均为 O(1)) ----------------
// 将节点添加到链表头部
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}

// 从链表中删除指定节点
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}

// 将节点移到头部(先删除,再添加到头部)
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}

// 删除尾部节点(最久未使用)
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}

posted @ 2026-05-06 11:09  ZealousMclaren  阅读(12)  评论(0)    收藏  举报