【Hot 100】 146. LRU 缓存

目录

  • 引言
  • [LRU 缓存](#LRU 缓存)
    • 官方解题
    • LRU实现
    • [📌 实现步骤分解](#📌 实现步骤分解)
      • [步骤 1:定义双向链表节点](#步骤 1:定义双向链表节点)
      • [步骤 2:创建伪头尾节点(关键设计)](#步骤 2:创建伪头尾节点(关键设计))
      • [步骤 3:实现链表基础操作](#步骤 3:实现链表基础操作)
        • [操作 1:添加节点到头部](#操作 1:添加节点到头部)
        • [操作 2:移除任意节点](#操作 2:移除任意节点)
      • [步骤 4:实现关键组合操作](#步骤 4:实现关键组合操作)
        • [操作 3:移动节点到头部(访问时调用)](#操作 3:移动节点到头部(访问时调用))
        • [操作 4:移除尾部节点(淘汰时调用)](#操作 4:移除尾部节点(淘汰时调用))
      • [步骤 5:初始化缓存结构](#步骤 5:初始化缓存结构)
      • [步骤 6:实现 get 操作](#步骤 6:实现 get 操作)
      • [步骤 7:实现 put 操作](#步骤 7:实现 put 操作)
    • [🔑 关键设计验证点](#🔑 关键设计验证点)
    • [🚀 完整实现代码](#🚀 完整实现代码)
    • [💡 实现要点总结](#💡 实现要点总结)
  • 🙋‍♂️ 作者:海码007
  • 📜 专栏:算法专栏
  • 💥 标题:【Hot 100】 146. LRU 缓存
  • ❣️ 寄语:书到用时方恨少,事非经过不知难!

引言

这题好像几年前就是hard。后面变成medium了。感觉就是普通人只做1~2遍,都不能独立记住整个实现过程。做到第3遍时大概能记得思路开始独立写代码了,但是会遇到各种问题不能bug free的AC掉。需要练很多遍才能真的在面试中写对的。这题应该就是靠代码功底的,看能不能现场写出bug free或者能debug出来。

上面的这个是别人写的评论,看着确实是这么回事。今天能把这道题写完就算ok了。这个相当于设计一个类了。

LRU 缓存

  • 🎈 题目链接:
  • 🎈 做题状态:

官方解题

这道题涉及的知识面确实比较多,第一次做的话不容易ac。可以多写几次。

cpp 复制代码
struct DLinkedNode {
    int key, value;
    DLinkedNode* prev;
    DLinkedNode* next;
    DLinkedNode() : key(0), value(0), prev(nullptr), next(nullptr) {}
    DLinkedNode(int _key, int _value) : key(_key), value(_value), prev(nullptr), next(nullptr) {}
};

class LRUCache {
private:
    unordered_map<int, DLinkedNode*> cache;
    DLinkedNode* head;
    DLinkedNode* tail;
    int size;
    int capacity;

public:
    LRUCache(int _capacity) : capacity(_capacity), size(0) {
        // 使用伪头部和伪尾部节点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head->next = tail;
        tail->prev = head;
    }
    
    int get(int key) {
        // 判断当前key是否存在
        if (!cache.count(key))
        {
            return -1;
        }

        // 通过哈希表快速的找到节点
        DLinkedNode* node = cache[key];
        moveToHead(node);
        return node->value;
    }
    
    void put(int key, int value) {
        // 判断当前key是否存在
        if (!cache.count(key))
        {
            // 不存在,则创建一个新节点
            DLinkedNode* node = new DLinkedNode(key, value);
            // 将其添加到哈希表中
            cache[key] = node;
            // 将新的节点添加到双向链表的头部,因为是新使用的
            addToHead(node);
            ++size;

            // 判断当前链表大小是否超过容量,如果超出容量,则需要删除尾部节点
            if (size > capacity)
            {
                DLinkedNode* removed = removedTail();
                cache.erase(removed->key);
                delete removed;
                --size;
            }
        }
        else
        {
            // 如果当前key存在,先通过哈希表定位,再修改value,并移动到头部
            DLinkedNode* node = cache[key];
            node->value = value;
            moveToHead(node);
        }
    }

    // 将节点添加到头部
    void addToHead(DLinkedNode* node)
    {
        node->prev = head;
        node->next = head->next;
        head->next->prev = node;
        head->next = node;
    }

    // 移除节点(不释放节点内存)
    void removeNode(DLinkedNode* node)
    {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    // 将节点移动到头部
    void moveToHead(DLinkedNode* node)
    {
        removeNode(node);
        addToHead(node);
    }

    // 将移除的节点返回给外部进行处理释放
    DLinkedNode* removedTail()
    {
        DLinkedNode* node = tail->prev;
        removeNode(node);
        return 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);
 */

LRU实现

我将把 LRU 缓存的实现拆解为 7 个清晰的步骤,并配以关键代码片段。让我们像搭积木一样逐步构建这个数据结构:


📌 实现步骤分解

步骤 1:定义双向链表节点

目的:创建存储数据的基本单元

cpp 复制代码
struct Node {
    int key;        // 用于删除尾节点时反向查找哈希表
    int value;
    Node* prev;
    Node* next;
    Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
};

步骤 2:创建伪头尾节点(关键设计)

目的:消除边界条件判断,简化链表操作

cpp 复制代码
class LRUCache {
private:
    Node* dummyHead; // 永远指向虚拟头部节点
    Node* dummyTail; // 永远指向虚拟尾部节点
    // 初始化伪头尾
    dummyHead = new Node(-1, -1);
    dummyTail = new Node(-1, -1);
    dummyHead->next = dummyTail;
    dummyTail->prev = dummyHead;

步骤 3:实现链表基础操作

操作 1:添加节点到头部
cpp 复制代码
void addToHead(Node* node) {
    // 新节点的前后指针
    node->prev = dummyHead;
    node->next = dummyHead->next;
    
    // 调整原有链接
    dummyHead->next->prev = node;
    dummyHead->next = node;
}
操作 2:移除任意节点
cpp 复制代码
void removeNode(Node* node) {
    node->prev->next = node->next;
    node->next->prev = node->prev;
}

步骤 4:实现关键组合操作

操作 3:移动节点到头部(访问时调用)
cpp 复制代码
void moveToHead(Node* node) {
    removeNode(node);  // 从当前位置移除
    addToHead(node);   // 添加到头部
}
操作 4:移除尾部节点(淘汰时调用)
cpp 复制代码
Node* removeTail() {
    Node* node = dummyTail->prev; // 真实尾节点
    removeNode(node);
    return node;
}

步骤 5:初始化缓存结构

cpp 复制代码
class LRUCache {
private:
    unordered_map<int, Node*> cache; // 哈希表
    int capacity;                    // 容量限制

public:
    LRUCache(int cap) : capacity(cap) {
        // 初始化伪头尾(见步骤2)
    }
};

步骤 6:实现 get 操作

cpp 复制代码
int get(int key) {
    if (!cache.count(key)) return -1; // 未命中
    
    Node* node = cache[key];
    moveToHead(node);    // 更新为最近使用
    return node->value;
}

步骤 7:实现 put 操作

cpp 复制代码
void put(int key, int value) {
    if (cache.count(key)) { // 已存在
        Node* node = cache[key];
        node->value = value;    // 更新值
        moveToHead(node);       // 设为最新
    } else {                   // 新增数据
        Node* node = new Node(key, value);
        cache[key] = node;      // 记录到哈希表
        addToHead(node);        // 添加到链表
        
        if (cache.size() > capacity) {
            Node* removed = removeTail();  // 淘汰最旧
            cache.erase(removed->key);     // 从哈希表删除
            delete removed;                // 释放内存
        }
    }
}

🔑 关键设计验证点

测试场景 预期操作 验证方法
缓存未满时插入 新节点添加到头部 检查链表头部是否为最新节点
缓存已满时插入 淘汰尾节点后添加新节点到头部 检查链表长度是否保持 capacity
访问已有节点 节点被移动到头部 执行get后检查节点位置
更新已有节点的值 值被更新且节点移到头部 检查值变化和节点位置

🚀 完整实现代码

cpp 复制代码
#include <unordered_map>
using namespace std;

struct Node {
    int key, value;
    Node *prev, *next;
    Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
};

class LRUCache {
private:
    unordered_map<int, Node*> cache;
    Node *dummyHead, *dummyTail;
    int capacity;

    void addToHead(Node* node) {
        node->prev = dummyHead;
        node->next = dummyHead->next;
        dummyHead->next->prev = node;
        dummyHead->next = node;
    }

    void removeNode(Node* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    void moveToHead(Node* node) {
        removeNode(node);
        addToHead(node);
    }

    Node* removeTail() {
        Node* node = dummyTail->prev;
        removeNode(node);
        return node;
    }

public:
    LRUCache(int cap) : capacity(cap) {
        dummyHead = new Node(-1, -1);
        dummyTail = new Node(-1, -1);
        dummyHead->next = dummyTail;
        dummyTail->prev = dummyHead;
    }

    int get(int key) {
        if (!cache.count(key)) return -1;
        Node* node = cache[key];
        moveToHead(node);
        return node->value;
    }

    void put(int key, int value) {
        if (cache.count(key)) {
            Node* node = cache[key];
            node->value = value;
            moveToHead(node);
        } else {
            Node* node = new Node(key, value);
            cache[key] = node;
            addToHead(node);
            if (cache.size() > capacity) {
                Node* removed = removeTail();
                cache.erase(removed->key);
                delete removed;
            }
        }
    }
};

💡 实现要点总结

  1. 双数据结构协作

    • 哈希表:O(1) 时间查找
    • 双向链表:维护访问顺序
  2. 伪节点的妙用

    • 消除头尾节点的特殊判断
    • 统一所有节点的操作逻辑
  3. 操作原子化

    • 将链表操作分解为独立方法
    • 提高代码可读性和可维护性
  4. 内存管理

    • 淘汰节点时需手动释放内存
    • 插入新节点时动态分配内存

通过这种分步实现方式,可以更清晰地理解每个组件的作用,也便于在开发过程中逐步测试验证每个功能的正确性。

相关推荐
EanoJiang2 小时前
查找
算法
今天也是元气满满的一天呢4 小时前
java学习之数据结构:一、数组
java·数据结构·学习
工藤新一¹4 小时前
C++/SDL 进阶游戏开发 —— 双人塔防(代号:村庄保卫战 20)
c++·单例模式·游戏引擎·sdl·c++游戏开发
钢铁男儿4 小时前
C# 方法(控制流和方法调用)
算法
heyCHEEMS5 小时前
最大子段和 Java
java·开发语言·算法
我是一只鱼02235 小时前
LeetCode算法题 (设计链表)Day16!!!C/C++
数据结构·c++·算法·leetcode·链表
hi0_65 小时前
Linux 第六讲 --- 工具篇(一)yum/apt与vim
linux·服务器·c++·vim·yum
wjm0410065 小时前
C++八股--5--设计模式--适配器模式,代理模式,观察者模式
c++·设计模式·适配器模式