【LRU缓存机制】+ 双向链表一些基础操作

news2024/10/5 23:27:46

文章目录

  • Tag
  • 题目来源
  • 题目解读
  • 解题方法
    • 方法一:哈希表+双向链表
  • 知识回顾
    • 双向链表的几个基本操作
  • 写在最后

Tag

【哈希表】【双向链表】【设计数据结构】【2023-09-24】


题目来源

146. LRU 缓存

146题目.gif


题目解读

LRU 是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。

本题需要设计实现 LRUCache 类,具体地需要实现:

  • LRUCache(int capacity):以正整数作为容量 capacity 初始化 LRU 缓存;
  • int get(int key):如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
  • void put(int key, int value):如果关键字 key 已经存在,则变更其数据值 value;如果不存在,则向缓存中插入该组 key-value。如果插入操作导致关键字数量超过 capacity,则应该 逐出 最久未使用的关键字。

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


解题方法

今天又是认真学习研究 LRU缓存机制 官方题解的一天!

方法一:哈希表+双向链表

使用什么样的数据结构?

对于设计这种题目,要明确每个步骤的时间复杂度要求,如果数据给定的操作是常数级别的,那么这个操作可用 O ( n ) O(n) O(n) 的算法;否则就要往 O ( 1 ) O(1) O(1) 或者 O ( l o g n ) O(logn) O(logn) 去考虑;

  • 对于创建操作 LRUCacheCreate,只有一次操作,一般就是 O ( n ) O(n) O(n) 了;
  • 对于获取操作 LRUCacheGet,如果要求 O ( 1 ) O(1) O(1),一般就是数组和哈希表了(大概率就是哈希表了);
  • 对于插入操作 LRUCachePut,如果要求 O ( 1 ) O(1) O(1),数组放入最后一个位置和链表放入第一个元素的操作都是 O ( 1 ) O(1) O(1)

如果插入的关键字数量超过 capacity,那么就应该逐出最久未使用的关键字。这表明插入和删除操作要在头部和尾部进行,能够在头部和尾部进行插入和删除操作的是队列,但是双向链表最佳。

具体实现

最终使用的数据结构是双向链表和哈希表。具体地:

  • 哈希表的键为 key,对应的值为 key 在双向链表中的位置;
  • 双向链表按照被使用的顺序存储了这些键值对,靠近双向链表头部的键值对表示最近使用的,靠近双向链表尾部的键值对表示最久未使用的。

这样,我们可以先通过哈希表来确定某一个 key 在缓存中的位置,访问了这个 key 之后,这个 key 就成为了最近访问的,就需要 移动到双向链表的头部,对应的操作就是 get 操作。具体如下:

  • 如果 key 不存在,则返回 -1
  • 如果 key 存在,则 key 对应的链表节点就是最近被使用的节点。需要将其在双向链表中的位置移动到头部,最后要返回该节点的值。

对于 put 操作,首先需要判断 key 是否存在:

  • 如果 key 不存在,需要使用 keyvalue 创建一个新的节点,将新建的节点加入到双向链表的头部(表示最近使用的),并将 key 和该节点加入到哈希表中。加入了一个新的双向链表节点之后需要判断是否超出了缓存的容量,如果超出了需要将双向链表的尾部节点删除(表示删除最近未使用的),并删除哈希表中的对应项;
  • 如果 key 存在,需要先通过哈希表定位,再将对应的节点值更新为 value,并将该节点移动到双向链表的头部。

上述各项操作中,访问哈希表的时间复杂度为 O ( 1 ) O(1) O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O ( 1 ) O(1) O(1)。而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作,都可以在 O ( 1 ) O(1) O(1) 时间内完成。

实现代码

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, *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) {
        if (!cache.count(key)) {
            return -1;
        }

        // key 存在,定位,移到头部
        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 = removeTail();
                cache.erase(removed->key);
                delete removed;
                --size;
            }

        }
        else { // key 存在,定位,修改,移到头部
            DLinkedNode* node = cache[key];
            node->value = value;
            moveToHead(node);
        }
    }

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

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

    // 删除双向链表的尾节点并返回删除的尾节点
    DLinkedNode* removeTail() {
        DLinkedNode* node = tail->prev;
        removeNode(node);
        return node;
    }

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

/**
 * 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);
 */

复杂度分析

时间复杂度:对于 putget 都是 O ( 1 ) O(1) O(1)

空间复杂度: O ( c a p a c i t y ) O(capacity) O(capacity),因为哈希表和双向链表最多存储 c a p a c i t y + 1 capacity+1 capacity+1 个元素。


知识回顾

双向链表的几个基本操作

接下来以图示的方式,来介绍一下上述成员方法实现中的一些双链表操作,包括:

  • 将节点 node 增加到双向链表头部;
  • 在双向链表中移除某个节点 node
  • 其他的一些操作(移除尾结点,移动节点到头部)都可以通过以上两种操作实现。

初始化

在双向链表的实现中,使用一个伪头部和伪尾部来标记界限,这样在增加节点和删除节点的时候就不要检查相邻两个节点是否存在了。

struct DLinkedNode {
    int value;
    DLinkedNode* prev;
    DLinkedNode* next;
    DLinkedNode(): value(0), prev(nullptr), next(nullptr) {}
    DLinkedNode(int _value): key(_key), value(_value), prev(nullptr), next(nullptr) {}
};
class UseDLinkedNode {
public:
    UseDLinkedNode() {
        // 使用伪头部和伪尾部节点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head->next = tail;
        tail->prev = head;
    }

    void addToHead(DLinkedNode* node);      // node 头插
    void removeNode(DLinkedNode* node);     // 删除 node
    void moveToHead(DLinkedNode* node);
    DLinkedNode* removeTail();
};

将节点 node 增加到双向链表头部

该操作就是将 node 插入到 dummy headdummy tail 之间:

(1)首先将 nodeprevnext 指针更新好,即 node->prev = headnode->next = head->next

(2)设置伪头部下一个节点的 prev(现在伪头节点下一个节点为伪尾部)节点,首先定位到伪头部下一个节点即 head->next

(3)伪头部下一个节点的 prevnode

(4)连接伪头部的下一个节点;

(5)最后,将节点 node 加入到双向链表头部即 node 的头插操作完成。

在双向链表中移除节点 node

在双向链表中移除某个节点,只需要修改指针的指向,使得双链表跳过该节点。

void UseDLinkedNode::removeNode(DLinkedNode* node) { // 删除 node
    node->prev->next = node->next;
    node->next->prev = node->prev;
}    

(1)修改 node->prev 的下一个节点的指向即 node->prev->next = node->next

(2)修改 node->next 的前一个节点的指向即 node->next->prev = node->prev

(3)最后删除 node 后的结果如下图所示。

移除尾结点

DLinkedNode* UseDLinkedNode::removeTail() {
    DLinkedNode* node = tail->prev; // 先找到
    removeNode(node);               // 再移除
    return node;                    // 最后返回被移除的尾节点
}

移动节点到头部

void UseDLinkedNode::moveToHead(DLinkedNode* node) {
    removeNode(node);   // 先移除
    addToHead(node);    // 加到头部
}

写在最后

如果文章内容有任何错误或者您对文章有任何疑问,欢迎私信博主或者在评论区指出 💬💬💬。

如果大家有更优的时间、空间复杂度方法,欢迎评论区交流。

最后,感谢您的阅读,如果感到有所收获的话可以给博主点一个 👍 哦。

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

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

相关文章

C语言每日一题(4):打印二进制的奇数位和小数位

文章主题&#xff1a;打印二进制的奇数位和小数位&#x1f525;所属专栏&#xff1a;C语言每日一题&#x1f4d7;作者简介&#xff1a;每天不定时更新C语言的小白一枚&#xff0c;记录分享自己每天的所思所想&#x1f604;&#x1f3b6;个人主页&#xff1a;[₽]的个人主页&…

干洗店收银管理软件,洗鞋店收银系统干洗app

干洗店收银管理软件&#xff0c;洗鞋店收银系统干洗app&#xff0c;支持上门取衣服干洗&#xff0c;在手机上下单&#xff0c;预约合适的时间&#xff0c;就会有专员来上门取&#xff0c;当然&#xff0c;送衣服务也是有的&#xff0c;一些价格都标注清楚&#xff0c;有更多的参…

YOLOv5改进系列(24)——替换主干网络之MobileViTv3(移动端轻量化网络的进一步升级)

【YOLOv5改进系列】前期回顾: YOLOv5改进系列(0)——重要性能指标与训练结果评价及分析 YOLOv5改进系列(1)——添加SE注意力机制 YOLOv5改进系列(2)——添加CBAM注意力机制

Stellar Toolkit for MySQL 9.0 Crack 3in1

面向数据库管理员的 MySQL 工具包 Stellar Toolkit for MySQL是一款三合一软件套件&#xff0c;用于修复损坏的 MySQL 和 MariaDB 数据库、从 MySQL 数据库的 InnoDB 和 MyISAM 表恢复数据以及分析 MySQL 数据库日志文件。该软件还可以以最高的安全性和完整性相互转换 MySQL/Ma…

pycharm中配置torch

在控制台cmd中安装好torch后&#xff0c;在pycharm中使用torch&#xff0c;需要进行简单设置即可。 在pycharm中新建一个工程&#xff0c;在file文件中打开setting 在setting中找到project interpreter编译器 找到conda environment的环境配置&#xff0c;设置好相应的目录 新…

一些框架使用总结

一.力软 1. 菜单相关 1) 页面菜单配置 自己写的页面 都在 src/modules中&#xff0c;基本结构如下图&#xff0c;具体页面在 views 文件夹中 module.js中&#xff0c;code字段需要和文件夹名称对应起来 export default {name: 案例演示模块,code: demo,version: 1.0.0,desc…

外卖霸王餐小程序、H5、公众号版外卖系统源码

最新外卖霸王餐小程序、H5、微信公众号版外卖系统源码、霸王餐美团、饿了么系统&#xff0c;粉丝裂变玩源码下载&#xff0c;外卖cps小程序项目&#xff0c;外卖红包cps带好友返利佣金分销系统程序、饿了么美团联盟源码&#xff0c;外卖cps带分销返利后端源码&#xff0c;基于L…

短视频账号系统源码(saas开发型)

抖音账号|短视频矩阵分发系统 | 多账号管理发布 |MVC架 一、短视频矩阵分发系统是什么&#xff1f; 短视频矩阵分发系统是一种集多账号、平台管理和视频分发于一身的技术运营类saas工具。它可以帮助用户管理多个账号&#xff0c;并将短视频快速传播到这些账号所在的不同平台上…

四川眼科医院孙丰源教授团队为患者拔除1.4cm长“眼中钉”

在户外劳作进行一些危险性的操作时&#xff0c;如果不注意防护&#xff0c;就很容易造成一些意外事件发生。广元的张先生使用割草机除草时&#xff0c;被割草机断裂的锯片击伤了左眼&#xff0c;伤势严重&#xff0c;所幸在孙丰源教授团队的帮助下&#xff0c;及时获得了治疗&a…

【操作系统笔记十】缓存一致性

CPU 核心之间数据如何传播 高速缓存中的值被修改了&#xff0c;那么怎么同步到内存中呢&#xff1f; ① 写直达&#xff08;Write-Through&#xff09;② 写回&#xff08;Write-Back&#xff09; 写直达&#xff08;Write-Through&#xff09; 简单&#xff0c;但是很慢&am…

Zygote Secondary:加速应用启动的未来之路

Zygote Secondary&#xff1a;加速应用启动的未来之路 1. 引言 在现代的移动应用开发中&#xff0c;启动速度和响应性能是用户体验的重要方面。然而&#xff0c;传统的 Android 进程管理方式在启动应用时会出现性能瓶颈&#xff0c;导致启动时间过长和资源占用过多。为了解决…

【WSN】无线传感器网络模拟器研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

EFK代替ELK方案7.17.3

文章目录 一. 传统的ELK二. EFK2.1 安装elasticsearch2.2 服务端安装fileBeats2.2.1. 安装 该也没有必要安装docker,直接下载yum或官网jar包启动即可.2.2.2.编辑配置文件 filebeat-java-logback.yml2.2.3. es配置common_log_pipeline解析日志 三.启动测试-logback-spring.xml配…

JavaWeb 学习笔记 6:会话跟踪

JavaWeb 学习笔记 6&#xff1a;会话跟踪 HTTP 协议本身是无状态的&#xff0c;所以不能跟踪会话状态。所以会有额外的技术用于跟踪会话&#xff1a; Cookie&#xff0c;客户端技术Session&#xff0c;服务端技术 1.Cookie 1.1.写入 Cookie 可以在服务端通过HttpServletRe…

如何使用Spring Security进行身份验证和授权

当您构建一个基于 Spring 框架的 Web 应用程序时&#xff0c;安全性是至关重要的。Spring Security 是 Spring 生态系统中用于处理身份验证和授权的框架。它提供了一种简单而强大的方式来保护您的应用程序&#xff0c;确保只有授权用户才能访问敏感资源。本文将介绍如何使用 Sp…

云计算安全:保护数字资产的前沿策略

文章目录 1. 云计算安全威胁1.1 数据泄露1.2 身份认证问题1.3 无法预测的网络攻击1.4 集中攻击 2. 云计算安全最佳实践2.1 身份和访问管理&#xff08;IAM&#xff09;2.2 数据加密2.3 安全审计和监控2.4 多重身份验证&#xff08;MFA&#xff09; 3. 安全自动化3.1 基础设施即…

【初试433分】中科院859学姐经验分享

这个系列会邀请往届学长学姐进行经验分享~欢迎后台回复经验分享&#xff0c;进行投稿&#xff01; 经验贴征集&#xff1a;前人栽树&#xff0c;后人乘凉&#xff0c;上岸同学也是看着经验贴一点一点过来的&#xff0c;有偿征集各位同学的经验分享&#xff0c;以此来帮助更多的…

一百八十四、大数据离线数仓完整流程——步骤三、在Hive中建基础库维度表并加载MySQL中的维度表数据

一、目的 经过6个月的奋斗&#xff0c;项目的离线数仓部分终于可以上线了&#xff0c;因此整理一下离线数仓的整个流程&#xff0c;既是大家提供一个案例经验&#xff0c;也是对自己近半年的工作进行一个总结。 二、数仓实施步骤 &#xff08;三&#xff09;步骤三、在Hive中…

优化类问题概述

数学建模系列文章&#xff1a; 以下是个人在准备数模国赛时候的一些模型算法和代码整理&#xff0c;有空会不断更新内容&#xff1a; 评价模型&#xff08;一&#xff09;层次分析法&#xff08;AHP&#xff09;,熵权法&#xff0c;TOPSIS分析 及其对应 PYTHON 实现代码和例题…