【LeetCode】146. LRU 缓存

题目描述

【LeetCode】146. 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 ,则应该 逐出 最久未使用的关键字。

函数 getput 必须以 O(1) 的平均时间复杂度运行。

示例:

复制代码
输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4

提示:

  • 1 <= capacity <= 3000
  • 0 <= key <= 10000
  • 0 <= value <= 105
  • 最多调用 2 * 105getput

核心思路

LRU的核心是淘汰最久未使用的数据,需要高效实现两个操作:

  1. 快速访问数据(get 操作);
  2. 快速插入/更新数据,并在满容量时快速删除最久未使用的数据(put 操作)。

最优方案是哈希表 + 双向链表的结合:

  • 双向链表 :维护数据的使用顺序,最近使用的放在头部,最久未使用的放在尾部
  • 哈希表:键为缓存的key,值为双向链表的节点,实现O(1)时间查找节点。

代码实现

java 复制代码
class LRUCache {
    // 双向链表节点:存储key、value,以及前后指针
    class Node {
        int key;
        int value;
        Node prev;
        Node next;
        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

    private int capacity;  // 缓存容量
    private Map<Integer, Node> cache;  // 哈希表:key -> 节点
    private Node head;  // 哨兵头节点(最近使用的节点在头部附近)
    private Node tail;  // 哨兵尾节点(最久未使用的节点在尾部)

    public LRUCache(int capacity) {
        this.capacity = capacity;
        cache = new HashMap<>(capacity);
        // 初始化哨兵节点,简化边界处理
        head = new Node(-1, -1);
        tail = new Node(-1, -1);
        head.next = tail;
        tail.prev = head;
    }
    
    public int get(int key) {
        if (!cache.containsKey(key)) {
            return -1;  // 键不存在,返回-1
        }
        // 键存在:获取节点,移动到头部(标记为最近使用)
        Node node = cache.get(key);
        moveToHead(node);
        return node.value;
    }
    
    public void put(int key, int value) {
        if (cache.containsKey(key)) {
            // 键存在:更新值,移动到头部(标记为最近使用)
            Node node = cache.get(key);
            node.value = value;
            moveToHead(node);
            return;
        }
        // 键不存在:检查容量
        if (cache.size() == capacity) {
            // 容量满:删除最久未使用的节点(尾节点的前一个)
            Node oldest = tail.prev;
            removeNode(oldest);
            cache.remove(oldest.key);  // 哈希表同步删除
        }
        // 插入新节点:添加到头部,哈希表记录
        Node newNode = new Node(key, value);
        addToHead(newNode);
        cache.put(key, newNode);
    }

    // 辅助方法:将节点移动到头部(先删除再添加到头部)
    private void moveToHead(Node node) {
        removeNode(node);
        addToHead(node);
    }

    // 辅助方法:删除节点
    private void removeNode(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    // 辅助方法:将节点添加到头部(head的后面)
    private void addToHead(Node node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

算法详解

1. 数据结构设计

  • 双向链表 :每个节点包含 keyvalueprev(前指针)、next(后指针)。通过链表维护使用顺序:
    • 最近使用的节点靠近 head(头哨兵);
    • 最久未使用的节点靠近 tail(尾哨兵)。
  • 哨兵节点headtail 不存储实际数据,用于简化边界处理(如空链表、删除首/尾节点)。
  • 哈希表cache 映射 key 到链表节点,实现O(1)时间查找。

2. 核心操作解析

get(key) 操作
  1. key 不在哈希表中,返回 -1
  2. key 存在,通过哈希表找到对应节点;
  3. 调用 moveToHead(node) 将节点移动到链表头部(标记为"最近使用");
  4. 返回节点的 value
put(key, value) 操作
  • 键已存在
    1. 找到对应节点,更新 value
    2. 调用 moveToHead(node) 标记为"最近使用"。
  • 键不存在
    1. 若缓存满(cache.size() == capacity):
      • 找到最久未使用的节点(tail.prev);
      • 调用 removeNode(oldest) 从链表中删除;
      • 从哈希表中删除该节点的 key
    2. 创建新节点,调用 addToHead(newNode) 添加到链表头部;
    3. 将新节点加入哈希表。

3. 辅助方法作用

  • removeNode(node):从链表中删除指定节点(调整前后节点的指针)。
  • addToHead(node):将节点添加到 head 后面(成为"最新使用"的节点)。
  • moveToHead(node):先删除节点,再添加到头部(更新使用顺序)。

复杂度分析

  • 时间复杂度getput 操作均为 O(1)。哈希表查找是O(1),双向链表的插入/删除操作也是O(1)。
  • 空间复杂度O(capacity) 。最多存储 capacity 个节点,哈希表和链表的空间均与容量成正比。

示例演示

capacity = 2 为例:

  1. put(1, 1) → 缓存:{1:1}(链表:head <-> 1 <-> tail);
  2. put(2, 2) → 缓存:{1:1, 2:2}(链表:head <-> 2 <-> 1 <-> tail);
  3. get(1) → 返回1,链表更新为:head <-> 1 <-> 2 <-> tail(1变为最近使用);
  4. put(3, 3) → 容量满,删除最久未使用的2,缓存:{1:1, 3:3}(链表:head <-> 3 <-> 1 <-> tail);
  5. get(2) → 返回-1(已被删除)。
相关推荐
葫芦和十三13 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp13 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑14 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯15 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan17 小时前
多Agent之间的区别
后端
青石路18 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充19 小时前
1.面向对象设计思想
后端
IT_陈寒19 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro20 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端