[数据结构] LRU Cache | ListMap 实现

server/2025/3/16 16:13:55/

目录

1. 什么是 LRU Cache

2. LRU Cache 的实现

3. 代码

概念理解

题目要求

删除一个节点(抽出一本书)

在链表头添加一个节点(把一本书放在最上面)

如何快速找到要抽出来的书?

答疑

代码实现

复杂度分析

4. 测试


1. 什么是 LRU Cache

定义:

  • LRU 是 Least Recently Used(最近最少使用)的缩写,它是一种缓存替换算法

Cache 的概念:

  • 在狭义上,Cache 指的是位于 CPU 和主存之间的快速 RAM,通常使用 SRAM 技术而非 DRAM,以提供更快的数据访问速度。
  • 广义上来讲,Cache 是指任何用于协调两种速度差异较大的硬件之间数据传输速度的结构。例如,CPU 与主存、内存与硬盘、甚至硬盘与网络之间的缓存。

  • 替换策略:
    • 当缓存容量达到上限而需要加入新内容时,LRU Cache 根据“最久未使用”的原则选择要移除的内容,即移除一段时间内最久没有被访问过的数据项。
    • 这种策略确保了缓存中保留的是最常或最近使用的数据。

2. LRU Cache 的实现

题目背景:

  • 我们可以通过解决 OJ 题目 146. LRU 缓存 来学习 LRU Cache 的实现方法。此题要求设计的数据结构必须支持在平均 O(1) 时间复杂度下完成 putget 操作。

初始化:

  • LRUCache(int capacity) 使用正整数作为容量来初始化缓存。这里的容量是指可以存储的关键字数量,当插入新的关键字导致总数超过容量时,应删除最近最少使用的关键字。

操作解析:

  • Get 操作: 给定一个关键字(实际上是一个地址),检查该关键字是否存在于缓存中。如果存在,则返回对应的数据值,并更新其为最近使用。
  • Put 操作: 包含两种情况
    • 一是更新现有关键字的值
    • 二是添加新的关键字-值对。
    • 如果添加新关键字导致缓存超出容量,则需先删除最久未使用的元素。

结构设计:

  • 哈希表 (unordered_map): 为了满足查找、新增和更新均为 O(1) 的时间复杂度,我们使用哈希表存储关键字到链表节点迭代器的映射。
  • 双向链表 (list): 用来维护元素的使用顺序。每次访问某个关键字后,将对应的节点移到链表头部;当需要移除元素时,总是移除链表尾部的节点(即最近最少使用的元素)。

优化点:

  • 使用 unordered_map<int, list< pair<int, int> >::iterator> 将关键字映射到链表中的位置,这样可以保证所有操作的时间复杂度为 O(1)。
  • 利用 splice 方法直接在链表中移动节点,避免不必要的创建和销毁节点的操作,提高效率。


3. 代码

概念理解

想象你有一摞书。

  • get:把一本书(key)抽出来,放在最上面。
  • put:放入一本新书。
    • 如果已经有这本书(key),就把它抽出来放在最上面,并替换它的 value。(例如把一本书的第二版替换成第三版)
    • 如果没有这本书(key),就放在最上面。
    • 如果超过 capacity 本书,就把最下面的书移除。
题目要求
  • get 和 put 都是 O(1) 时间复杂度,这可以用双向链表实现,每个节点都表示一本书(key 和 value)。
  • 每个节点都有两个指针 prevnext 分别指向前一个和下一个节点。
  • 此外,链表中还包含一个哨兵节点 dummy,这可以让每个节点的 prevnext 都不为空,从而简化代码逻辑。
删除一个节点(抽出一本书)

  • 删除节点 x 之前:
    • x.prev.next = x.next
    • x.next.prev = x.prev
在链表头添加一个节点(把一本书放在最上面)

  • 插入节点 x 之前:
    • x.prev = dummy
    • x.next = dummy.next
    • x.prev.next = x
    • x.next.prev = x
如何快速找到要抽出来的书?
  • key 映射链表中的对应节点。这可以用哈希表实现。
答疑
  • 问:需要几个哨兵节点?
    • 答:一个就够了。一开始哨兵节点 dummyprevnext 都指向 dummy。随着节点的插入,dummynext 指向链表的第一个节点(最上面的书),prev 指向链表的最后一个节点(最下面的书)。
  • 问:为什么节点要把 key 也存下来?
    • 答:在删除链表末尾节点时,也要删除哈希表中的记录,这需要知道末尾节点的 key。
代码实现
class Node {
public:int key;int value;Node* prev;Node* next;Node(int k = 0, int v = 0): key(k), value(v), prev(nullptr), next(nullptr) {}
};class LRUCache {
private:int capacity;Node* dummy; // 哨兵节点unordered_map<int, Node*> key_to_node;void remove(Node* x) {x->prev->next = x->next;x->next->prev = x->prev;}void push_front(Node* x) {x->prev = dummy;·x->next = dummy->next;x->prev->next = x;x->next->prev = x;}Node* get_node(int key) {auto it = key_to_node.find(key);if (it == key_to_node.end()) {return nullptr;}Node* node = it->second;remove(node);push_front(node);return node;}public:LRUCache(int capacity): capacity(capacity), dummy(new Node()) {dummy->prev = dummy;dummy->next = dummy;}int get(int key) {Node* node = get_node(key);return node ? node->value : -1;}void put(int key, int value) {Node* node = get_node(key);if (node) {node->value = value;return;}key_to_node[key] = node = new Node(key, value);push_front(node);if (key_to_node.size() > capacity) {Node* back_node = dummy->prev;key_to_node.erase(back_node->key);remove(back_node);delete back_node;}}
};
复杂度分析
  • 时间复杂度:所有操作均为 O(1)。
  • 空间复杂度:O(min(p, capacity)),其中 p 为 put 的调用次数。

4. 测试

#include <iostream>
#include <unordered_map>#include"LRUCache.hpp"void testLRUCache() {LRUCache cache(2);cache.put(1, 1);cache.put(2, 2);std::cout << "Get key 1: " << (cache.get(1) == 1 ? "Pass" : "Fail") << std::endl;cache.put(3, 3);std::cout << "Get key 2: " << (cache.get(2) == 2 ? "Pass" : "Fail") << std::endl;cache.put(4, 4);std::cout << "Get key 1: " << (cache.get(1) == 1 ? "Pass" : "Fail") << std::endl;std::cout << "Get key 3: " << (cache.get(3) == 3 ? "Pass" : "Fail") << std::endl;std::cout << "Get key 4: " << (cache.get(4) == 4 ? "Pass" : "Fail") << std::endl;
}int main() {testLRUCache();return 0;
}

运行:

通过上述设计,我们可以有效地实现一个高效且符合 LRU 替换策略的缓存系统,适用于多种应用场景,如数据库查询缓存、网页服务器响应缓存等。

代码设计参考:灵神的题解


http://www.ppmy.cn/server/152141.html

相关文章

岁末回望,追梦远方

又到了岁末年初&#xff0c;按惯例&#xff0c;风云我都会写一篇长长的感悟&#xff0c;给自己辞旧的总结复盘&#xff0c;迎新的追梦定调&#xff0c;今年赋诗一首&#xff0c;畅想一下诗和远方&#xff0c;简洁而又虚无&#xff0c;缥缈中坚定初心。 岁末回首步履深&#xf…

*【每日一题 基础题】 [蓝桥杯 2023 省 B] 飞机降落

题目描述 N 架飞机准备降落到某个只有一条跑道的机场。其中第 i 架飞机在 Ti 时刻到达机场上空&#xff0c;到达时它的剩余油料还可以继续盘旋 Di 个单位时间&#xff0c;即它最早可以于 Ti 时刻开始降落&#xff0c;最晚可以于 Ti Di 时刻开始降落。降落过程需要 Li个单位时间…

【Super Tilemap Editor使用详解】(七):图块集纹理编辑器(Tileset Atlas Editor)

1、创建图块集后&#xff0c;我们可以打开 Atlas Editor Window&#xff08;纹理编辑器窗口&#xff09;以修改图块集的纹理和配置。 2、打开的方法 &#xff0c;从菜单中选择 "SuperTilemapEditor/Window/Atlas Editor Window" 打开 3、图块集切片设置&#xff08;S…

Golang的向前兼容性和toolchain规则,Go1.21.0

在 Go 1.21 中&#xff0c;在工具上新增了两个变化&#xff1a;增强了向前兼容性&#xff1b;工具链管理。 向前兼容性 在以前的版本中&#xff0c;Go 工具链尝试编译依赖于新版本的代码时&#xff0c;可能会遇到兼容性问题。例如&#xff0c;如果你的代码依赖于 Go 1.18 引入…

纯血鸿蒙APP实战开发——应用新功能引导实现案例

应用新功能引导实现案例 介绍 本文介绍如何使用high_light_guide三方库完成应用新版本功能导航。通过高亮区域与蒙版背景的明暗度对比&#xff0c;让用户快速锁定重点功能&#xff0c;了解版本变更和业务入口。 效果图预览 使用说明 点击页面上对应按钮或空白区域进入下一个…

1-Gin介绍与环境搭建 --[Gin 框架入门精讲与实战案例]

Gin 介绍 Gin 是一个用 Go&#xff08;Golang&#xff09;编写的 Web 框架&#xff0c;它以极高的性能和简洁的 API 设计而闻名。Gin 的设计灵感来自于 Martini 框架&#xff0c;但它的性能更优&#xff0c;延迟更低&#xff0c;非常适合构建高效的微服务和 RESTful API 服务。…

2024年12月16日Github流行趋势

项目名称&#xff1a;PDFMathTranslate 项目维护者&#xff1a;Byaidu reycn hellofinch Wybxc YadominJinta项目介绍&#xff1a;基于 AI 完整保留排版的 PDF 文档全文双语翻译&#xff0c;支持 Google/DeepL/Ollama/OpenAI 等服务&#xff0c;提供 CLI/GUI/Docker。项目star数…

Django 提供的会话(Session)相关的设置说明

SESSION_COOKIE_AGE 说明&#xff1a;定义会话 Cookie 的有效时间&#xff0c;单位为秒。 默认值&#xff1a;1209600&#xff08;即 2 周&#xff09;。 示例&#xff1a; SESSION_COOKIE_AGE 3600 # 1小时SESSION_EXPIRE_AT_BROWSER_CLOSE …