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

函数 get 和 put 必须以 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

解题思路:

这题我卡在了如何维护访问顺序的问题上了。看了题解才知道其实思路不难。

方法一:哈希表 + 双向链表

为什么需要两个数据结构?

需求 数据结构 原因
O(1) 查找 key 哈希表 unordered_map 查找 O(1)
O(1) 维护访问顺序 双向链表 插入、删除、移动节点都是 O(1)

双向链表的角色:

  • 头部:最近使用的节点

  • 尾部:最久未使用的节点

  • 每次 get 或 put,把节点移到头部

  • 超出容量时,删除尾部节点

具体过程示例:

复制代码
哈希表: {1: node1, 2: node2, 3: node3}
双向链表: head <-> 3 <-> 1 <-> 2 <-> tail
                    ↑
              最近使用    最久未使用

get(1) 后: head <-> 1 <-> 3 <-> 2 <-> tail
put(4) 后: 容量超出,删除 tail 前一个(2)
          head <-> 4 <-> 1 <-> 3 <-> tail

代码实现:

cpp 复制代码
class LRUCache {
private:
    struct Node {
        int key, value;
        Node *prev, *next;
        Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
    };
    
    int capacity;
    unordered_map<int, Node*> cache;  // key → 节点指针
    Node *head, *tail;                // 虚拟头尾节点
    
    // 把节点移到头部(最近使用)
    void moveToHead(Node* node) {
        removeNode(node);
        addToHead(node);
    }
    
    // 从链表中删除节点
    void removeNode(Node* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }
    
    // 把节点加到头部
    void addToHead(Node* node) {
        node->next = head->next;
        node->prev = head;
        head->next->prev = node;
        head->next = node;
    }
    
    // 删除尾部节点(最久未使用)
    Node* removeTail() {
        Node* node = tail->prev;
        removeNode(node);
        return node;
    }
    
public:
    LRUCache(int capacity) : capacity(capacity) {
        head = new Node(0, 0);  // 虚拟头结点
        tail = new Node(0, 0);  // 虚拟尾节点
        head->next = tail;
        tail->prev = head;
    }
    
    int get(int key) {
        if (cache.find(key) == cache.end()) {
            return -1;
        }
        Node* node = cache[key];
        moveToHead(node);  // 移到头部,表示最近使用
        return node->value;
    }
    
    void put(int key, int value) {
        if (cache.find(key) != cache.end()) {
            // key 已存在,更新值并移到头部
            Node* node = cache[key];
            node->value = value;
            moveToHead(node);
        } else {
            // key 不存在,新建节点
            Node* node = new Node(key, value);
            cache[key] = node;
            addToHead(node);
            
            // 超出容量,删除尾部
            if (cache.size() > capacity) {
                Node* removed = removeTail();
                cache.erase(removed->key);
                delete removed;
            }
        }
    }
};

复杂度分析:

操作 时间复杂度 说明
get O(1) 哈希表查找 O(1) + 链表移动 O(1)
put O(1) 哈希表插入 O(1) + 链表操作 O(1)
空间复杂度 O(capacity) 哈希表 + 链表最多存 capacity 个节点

关键细节:

1. 为什么用虚拟头尾节点?
cpp 复制代码
head = new Node(0, 0);
tail = new Node(0, 0);
head->next = tail;
tail->prev = head;
  • 避免处理"链表为空"、"插入第一个节点"等边界情况

  • 所有节点的插入删除逻辑统一

2. get 和 put 都要调用 moveToHead

因为 get 也算"使用",被 get 的节点应该变成最近使用的。

3. 超出容量时删除尾部
cpp 复制代码
Node* removed = removeTail();
cache.erase(removed->key);
delete removed;

注意:先从哈希表中删除,再释放节点内存。

4. 为什么用双向链表而不是单向链表?

因为需要 O(1) 删除任意节点。单向链表删除节点需要知道前驱,双向链表可以直接通过 node->prev 找到前驱。

方法二:用 list + unordered_map(C++ STL 版)

代码实现:

cpp 复制代码
class LRUCache {
private:
    int capacity;
    list<pair<int, int>> cacheList;  // 存 (key, value)
    unordered_map<int, list<pair<int, int>>::iterator> cacheMap;
    
public:
    LRUCache(int capacity) : capacity(capacity) {}
    
    int get(int key) {
        if (cacheMap.find(key) == cacheMap.end()) return -1;
        
        // 移到头部
        cacheList.splice(cacheList.begin(), cacheList, cacheMap[key]);
        return cacheMap[key]->second;
    }
    
    void put(int key, int value) {
        if (cacheMap.find(key) != cacheMap.end()) {
            // 更新值并移到头部
            cacheMap[key]->second = value;
            cacheList.splice(cacheList.begin(), cacheList, cacheMap[key]);
        } else {
            // 插入新节点
            cacheList.push_front({key, value});
            cacheMap[key] = cacheList.begin();
            
            // 超出容量
            if (cacheList.size() > capacity) {
                cacheMap.erase(cacheList.back().first);
                cacheList.pop_back();
            }
        }
    }
};

优点 :用 STL 的 list 和 splice,代码更简洁

缺点 :需要理解 splice 的用法

两种方法对比:

方法 get put 代码复杂度 推荐度
手写双向链表 + 哈希表 O(1) O(1) 较高 ⭐⭐⭐⭐⭐
STL list + 哈希表 O(1) O(1) 中等 ⭐⭐⭐⭐

总结:

要点 说明
核心思想 哈希表 + 双向链表
哈希表作用 O(1) 查找 key
双向链表作用 O(1) 维护访问顺序
头部 最近使用
尾部 最久未使用
超出容量 删除尾部节点

这道题是设计题 的经典题,也是面试高频题。核心是哈希表 + 双向链表的组合。

相关推荐
灯澜忆梦4 小时前
【面向对象编程C++】| 基础语法
java·c++·算法
禾小西4 小时前
Redis 数据结构:快速的 Redis 有哪些慢操作?
数据结构·数据库·redis
小蒋观天下4 小时前
专项方案:大场景港口AI安防、多干扰环境下的算法调优与落地实操
人工智能·深度学习·算法·安全·机器学习·计算机视觉·ai大模型
Evand J4 小时前
【电机滤波例程5】渐消因子自适应扩展卡尔曼滤波(EKF),MATLAB。PMSM负载突变下的状态估计。附下载链接
算法·matlab
H.莓飛5 小时前
【数据结构】二叉树_OJ题
linux·开发语言·数据结构·算法
好想像大佬一样能够ak所有5 小时前
Leetcode35.搜索插入位置
数据结构·算法·leetcode
aqiu1111115 小时前
【C++算法打怪专栏】LeetCode 165. 比较版本号(双指针与字符串流法)
c++·算法·leetcode
黄金龙PLUS5 小时前
如何基于置换算法来设计对称加密算法
算法·网络安全·密码学·哈希算法·同态加密
不会就选b5 小时前
算法日常・每日刷题--<动态规划>14
算法
赛博守夜人6 小时前
跨境业务与出海合规之九:海外买量投放与营销虚假流量(Ad Fraud):跨国机房群控与归因欺诈识别算法
算法·安全