【Hot 100】 146. LRU 缓存

目录

  • 引言
  • LRU 缓存
    • 官方解题
    • LRU实现
    • 📌 实现步骤分解
      • 步骤 1:定义双向链表节点
      • 步骤 2:创建伪头尾节点(关键设计)
      • 步骤 3:实现链表基础操作
        • 操作 1:添加节点到头部
        • 操作 2:移除任意节点
      • 步骤 4:实现关键组合操作
        • 操作 3:移动节点到头部(访问时调用)
        • 操作 4:移除尾部节点(淘汰时调用)
      • 步骤 5:初始化缓存结构
      • 步骤 6:实现 get 操作
      • 步骤 7:实现 put 操作
    • 🔑 关键设计验证点
    • 🚀 完整实现代码
    • 💡 实现要点总结

请添加图片描述

  • 🙋‍♂️ 作者:海码007
  • 📜 专栏:算法专栏
  • 💥 标题:【Hot 100】 146. LRU 缓存
  • ❣️ 寄语:书到用时方恨少,事非经过不知难!

引言

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

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

LRU 缓存

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

官方解题

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

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:定义双向链表节点

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

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

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

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

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:添加节点到头部
void addToHead(Node* node) {// 新节点的前后指针node->prev = dummyHead;node->next = dummyHead->next;// 调整原有链接dummyHead->next->prev = node;dummyHead->next = node;
}
操作 2:移除任意节点
void removeNode(Node* node) {node->prev->next = node->next;node->next->prev = node->prev;
}

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

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

步骤 5:初始化缓存结构

class LRUCache {
private:unordered_map<int, Node*> cache; // 哈希表int capacity;                    // 容量限制public:LRUCache(int cap) : capacity(cap) {// 初始化伪头尾(见步骤2)}
};

步骤 6:实现 get 操作

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

步骤 7:实现 put 操作

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后检查节点位置
更新已有节点的值值被更新且节点移到头部检查值变化和节点位置

🚀 完整实现代码

#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. 内存管理

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/904622.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux】swap交换分区管理

目录 一、Swap 交换分区的功能 二、swap 交换分区的典型大小的设置 2.1 查看交换分区的大小 2.1.1 free 2.1.2 cat /proc/swaps 或 swapon -s 2.1.3 top 三、使用交换分区的整体流程 3.1 案例一 3.2 案例二 一、Swap 交换分区的功能 计算机运行一个程序首先会将外存&am…

【计算机网络】用户从输入网址到网页显示,期间发生了什么?

1.URL解析 浏览器分解URL&#xff1a;https://www.example.com/page 协议&#xff1a;https域名&#xff1a;www.example.com路径&#xff1a;/page 2.DNS查询&#xff1a; 浏览器向DNS服务器发送查询请求&#xff0c;将域名解析为对应的IP地址。 3.CDN检查(如果有)&#…

架空输电线巡检机器人轨迹优化设计

架空输电线巡检机器人轨迹优化 摘要 本论文针对架空输电线巡检机器人的轨迹优化问题展开研究,综合考虑输电线复杂环境、机器人运动特性及巡检任务需求,结合路径规划算法、智能优化算法与机器人动力学约束,构建了多目标轨迹优化模型。通过改进遗传算法与模拟退火算法,有效…

根据窗口大小自动调整页面缩放比例,并保持居中显示

vue 项目 直接上代码 图片u1.png 是个背景图片 图片u2.png 是个遮罩 <template><div id"app"><div class"viewBox"><divclass"screen":style"{ transform: translate(-50%,-50%…

初学Python爬虫

文章目录 前言一、 爬虫的初识1.1 什么是爬虫1.2 爬虫的核心1.3 爬虫的用途1.4 爬虫分类1.5 爬虫带来的风险1.6. 反爬手段1.7 爬虫网络请求1.8 爬虫基本流程 二、urllib库初识2.1 http和https协议2.2 编码解码的使用2.3 urllib的基本使用2.4 一个类型六个方法2.5 下载网页数据2…

oracle 数据库sql 语句处理过程

14.1SQL语句处理过程 在进行SQL语句处理优化前&#xff0c;需要先熟悉和了解SQL语句的处理过程。 每种类型的语句在执行时都需要如下阶段&#xff1a; 第1步: 创建游标。 第2步: 分析语句。 第5步: 绑定变量。 第7步: t运行语句。 第9步: 关闭游标。 如果使用了并行功能&#x…

pm2 list查询服务时如何通过name或者namespace进行区分

在 PM2 中&#xff0c;如果 pm2 list 显示的所有服务名称&#xff08;name&#xff09;相同&#xff0c;就无法直观地区分不同的进程。这时可以通过 --namespace&#xff08;命名空间&#xff09; 或 自定义 name 来区分服务。以下是解决方案&#xff1a; 方法 1&#xff1a;启…

[python] 函数基础

二 函数参数 2.1 必备参数(位置参数) 含义: 传递和定义参数的顺序及个数必须一致 格式: def func(a,b) def func_1(id,passwd):print("id ",id)print("passwd ",passwd) func_1(10086,123456) 2.2 默认参数 函数: 为函数的参数提供一个默认值,如果调…

超大规模SoC后仿真流程与优化

在超大规模SoC设计中,是否需要进行全芯片后仿真(Full-Chip Post-layout Simulation)取决于多个因素,包括设计复杂度、项目风险、资源限制以及验证目标。以下是针对这一问题的系统性分析: 1. 全芯片后仿真的必要性 需要全芯片后仿真的场景 系统级交互验证: 跨模块信号交互…

深入理解 Docker 网络原理:构建高效、灵活的容器网络

在现代软件开发中&#xff0c;Docker 已经成为了容器化技术的代名词&#xff0c;广泛应用于开发、测试和生产环境。Docker 使得开发者能够将应用及其依赖打包成一个轻量级的容器&#xff0c;并通过 Docker 容器化技术来实现高效的部署与管理。 然而&#xff0c;在日常使用 Dock…

leetcode 242. Valid Anagram

题目描述 因为s和t仅仅包含小写字母&#xff0c;所以可以开一个26个元素的数组用来做哈希表。不过如果是unicode字符&#xff0c;那就用编程语言自带的哈希表。 class Solution { public:bool isAnagram(string s, string t) {int n s.size();if(s.size() ! t.size())return …

4、反应釜压力监控系统 - /自动化与控制组件/reaction-vessel-monitor

76个工业组件库示例汇总 反应釜压力监控组件 这是一个用于反应釜压力监控的自定义组件&#xff0c;专为化工厂反应釜压力监控设计。采用苹果工业风格界面&#xff0c;简洁优雅&#xff0c;功能实用&#xff0c;易于使用。 功能特点 实时压力可视化&#xff1a;直观展示反应…

系统思考助力富维东阳

刚刚完成了长春一家汽车零配件公司关于系统思考的项目&#xff01; 在开班仪式上&#xff0c;公司总经理深刻阐述了项目的背后意义&#xff0c;强调了系统思考与公司战略的紧密联系。这不仅是一次培训&#xff0c;更是一次关于“如何全方位看待问题”的深度对话。 在这个过程中…

Linux下的c/c++开发之操作Sqlite3数据库

libsqlite3-dev 介绍&#xff08;Linux 下的 SQLite3 C/C 开发包&#xff09; libsqlite3-dev 是一个开发包&#xff0c;在 Linux 环境下为使用 SQLite3 C API 进行开发的 C/C 程序员提供头文件&#xff08;如 sqlite3.h&#xff09;和静态库/动态库的链接信息&#xff08;如 …

【Prompt工程—文生图】案例大全

目录 一、人物绘图 二、卡通头像 三、风景图 四、logo设计图 五、动物形象图 六、室内设计图 七、动漫风格 八、二次元图 九、日常场景图 十、古风神化图 十一、游戏场景图 十二、电影大片质感 本文主要介绍了12种不同类型的文生图技巧&#xff0c;通过加入不同的图像…

GMRES算法处理多个右端项的Block与PseudoBlock变体

GMRES算法处理多个右端项的Block与PseudoBlock变体 Block与PseudoBlock GMRES简介 在处理多个右端项的线性方程组时&#xff0c;Block GMRES和PseudoBlock GMRES是两种常用的变体算法&#xff1a; Block GMRES&#xff1a;同时处理所有右端项&#xff0c;构建一个大的Krylov…

Ubuntu环境下如何管理系统中的用户:创建用户、删除用户、修改密码、切换用户、用户组管理

管理用户的操作需要root权限&#xff0c;在执行命令时需要加sudo&#xff0c;关于sudo命令可以看这篇&#xff1a;Linux_sudo命令的使用与机制 1、添加用户 使用命令&#xff1a; adduser 用户名&#xff0c;主要是按提示输入密码和用户信息&#xff08;可直接回车使用默认配置…

开源BI选型及DataEase搭建

工具名称 国家/社区技术栈核心功能国内适用性国外适用性推荐场景Apache Superset美国&#xff08;Apache&#xff09;Python/React可视化、SQL Lab、多数据源、插件扩展需自行汉化&#xff0c;社区支持较少生态完善&#xff0c;云原生支持好&#xff08;AWS/GCP&#xff09;中大…

云计算-容器云-部署jumpserver 版本1

部署jumpserver [root@jumpserver ~]# tar -zxvf jumpserver.tar.gz -C /opt/ [root@jumpserver ~]# ls /opt/ compose config docker docker.service images jumpserver-repo static.env将默认Yum源移至其他目录,创建本地Yum源文件,命令及文件内容如下: [root@jumpserver…

利用Elixir中的原子特性 + 错误消息泄露 -- Atom Bomb

题目信息: This new atom bomb early warning system is quite strange… 题目使用 elixir 语言 一开始,我们会访问 /page.html <!DOCTYPE html> <!-- 设定文档语言为英语 --> <html lang"en"> <head><!-- 设定字符编码为UTF-8 --><…