当前位置: 首页 > article >正文

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

目录

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.kler.cn/a/446048.html

相关文章:

  • Android详解——ConstraintLayout约束布局
  • 【服务器】MyBatis是如何在java中使用并进行分页的?
  • 前端知识图谱 - JavaScript基础(变量和类型)
  • EasyGBS国标GB28181平台P2P远程访问故障排查指南:客户端角度的排查思路
  • Qt+OPC开发笔记(一):OPCUA介绍、open62541介绍、编译与基础环境Demo
  • javalock(四)AQS派生类之Semphore逐行注释
  • JAVA队列每次添加需要新实例才能独立更新
  • 基于python+vue开发的图书借阅网站
  • windows自带16进制转10进制
  • 【数据库MySQL篇三】MySQL数据库入门基础教程:一网打尽SQL命令和语法
  • springboot462学生心理压力咨询评判(论文+源码)_kaic
  • linux----系统i/o
  • 多智能体/多机器人网络中的图论法
  • Java:基于SSM的旅游攻略管理系统
  • electron opacity 百分比设置不生效 变成1% 问题
  • STM32读写flash注意事项
  • 自动化立体仓库堆垛机SRM控制系统货叉控制功能块开发设计
  • 【操作系统】每日 3 题(七十二)
  • CSS|10 内填充padding外边距margin
  • UDP系统控制器_音量控制、电脑关机、文件打开、PPT演示、任务栏自动隐藏
  • 深入解析 OpenSSH 的核心原理
  • 鸿蒙学习笔记:用户登录界面
  • 震撼!最强开源模型通义千问2.5 72B竟在4GB老显卡上成功运行!
  • 基于 Vue 3 实现无限滚动翻页组件
  • linux java 查看异常堆栈
  • 文件包含include