Hot 100 刷题计划】 LeetCode 146. LRU 缓存 | C++ 哈希表+双向链表

LeetCode 146. LRU 缓存

📌 题目描述

题目级别:中等 (实际面试中常作为 Hard 级别压轴)

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。

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


💡 破题思路:为什么需要哈希表 + 双向链表?

要想在 O(1) 的时间复杂度内完成查找和更新,我们必须使用两种数据结构的组合:

  1. 哈希表 (unordered_map) :负责满足 get 操作的 O(1) 查找。它能瞬间通过 key 定位到数据在内存中的具体位置。
  2. 双向链表 (Doubly Linked List) :负责维持数据的"时间先后顺序"。
    • 为什么不用单链表?因为当我们通过哈希表命中某个节点,想要把它移到头部时,单链表无法在 O(1) 时间内找到它的前驱节点来断开连接,而双向链表有 pre 指针,可以瞬间"原地拔起"。
    • 为什么不用数组?数组在头部插入或删除元素需要移动大量数据,复杂度是 O(N)。

运作机制:

  • 越靠近链表头部的节点,越是"最近使用"的。
  • 越靠近链表尾部的节点,越是"最久未使用"的。
  • 每次 get 命中一个节点,就把它从原位置拔出来,重新插到头部 (hinert)。
  • 每次 put 新数据,直接插到头部;如果容量超标,就把尾部节点 (tail->pre) 无情淘汰!

极客细节:虚拟头尾节点

人为在链表两端加上 head 和 tail 两个 Dummy 节点,这样任何真实的节点都有前驱和后继,写 remove 和 hinert 时再也不用去判断 if (node->pre != nullptr) 这种恶心的边界条件了。


💻 C++ 代码实现

cpp 复制代码
class Node{
public:
    Node *next;
    Node *pre;
    int key, value;
    Node (int k, int v) {
        key = k;
        value = v;
        next = NULL;
        pre = NULL;
    }
};

class LRUCache {
public:
    LRUCache(int capacity) {
        cap = capacity;
        // 初始化虚拟头尾节点,互相指引,彻底消除空指针异常边界
        head = new Node(0, 0);
        tail = new Node(0, 0);
        head -> next = tail;
        tail -> pre = head;
    }
    
    int get(int key) {
        if (mp.count(key))
        {
            // 只要被访问,就说明它是"最近使用"的,立刻把它移到链表头部
            remove(mp[key]);
            hinert(mp[key]);
            return mp[key] -> value;
        }

        return -1;
    }
    
    void put(int key, int value) {
        // 如果 key 已经存在,作者采取了极其果断的策略:
        // 删掉旧的,重新建个新的插到头部,逻辑异常清晰!
        if (mp.count(key))
        {
            remove(mp[key]);
            delete mp[key];
            mp[key] = NULL;
        }

        // 创建新节点,执行头插法 (hinert),并在 map 中记录位置
        Node *tmp = new Node(key, value);
        hinert(tmp);
        mp[key] = tmp;

        // 如果超出容量限制,启动 LRU 淘汰机制
        if (mp.size() > cap)
        {
            // 真实有效节点中,最久没使用的就是 tail 的前一个节点
            Node *todel = tail -> pre;
            remove(todel);           // 从链表中剥离
            mp.erase(todel -> key);  // 从哈希表中注销
            delete todel;            // 释放内存,防止内存泄漏
        }
    }

    // 辅助函数:将节点从双向链表中剥离
    void remove(Node *tmp)
    {
        tmp -> pre -> next = tmp -> next;
        tmp -> next -> pre = tmp -> pre;
    }

    // 辅助函数:头插法 (Head Insert),将节点插入到虚拟头节点之后
    void hinert(Node *tmp)
    {
        tmp -> next = head -> next;
        head -> next = tmp;
        tmp -> pre = head;
        tmp -> next -> pre = tmp;
    }

private:
    int cap;
    Node *head, *tail;
    unordered_map<int, Node*> mp; // Key 到 链表节点指针 的映射
};
相关推荐
个 人 练 习 生2 分钟前
C++ string 类模拟实现:从底层理解字符串(上)
开发语言·c++·经验分享·学习·程序人生
垆边人似月.9 分钟前
华为机试题 :有效的括号
c++·算法·华为
坤坤子吖14 分钟前
C++智能指针:RAII、shared_ptr与内存泄漏
开发语言·c++·笔记·学习
weixin1997010801639 分钟前
《1688图片空间API踩坑:img.upload 与 album.* 的防盗链与CDN缓存问题》(附Python源码)
开发语言·python·缓存
夏幻灵1 小时前
每日一题:LeetCode 206:反转链表——三指针与原地反转详解
算法·leetcode·链表
神仙别闹1 小时前
基于C++实现的贪吃蛇游戏平台
java·c++·游戏
hweiyu001 小时前
Redis命令:BLPOP
redis·缓存
王老师青少年编程1 小时前
2026年6月GESP真题及题解(C++三级): 选择题和判断题(题解)
c++·真题·gesp·答案·csp·三级·信奥赛
C++ 老炮儿的技术栈1 小时前
static的作用
c语言·c++·人工智能·单片机·c
程序员老陆3 小时前
C++ 未定义行为全景:对象模型、优化边界与工程防御
开发语言·c++·程序设计