数据结构---LRU CACHE

什么是LRU

通过之前的学习我们知道计算机在处理任务的时候是先将数据从硬盘中提取出来加载进内存,然后再将内存中的数据加载进入cpu进行计算,但是这里存在一个问题cpu的计算速度非常快,而内存中加载数据的速度又很慢,所以为了提供整机的工作效率我们就得在内存和cpu计算机中添加一个东西叫做缓存,狭义的Cache指的是位于CPU和主存间的快速RAM(缓存),通常它不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术。 广义上的Cache指的是位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构。除了CPU与主存之间有Cache,内存与硬盘之间也有Cache,乃至在硬盘与网络之间也有某种意义上的Cache── 称为Internet临时文件夹或网络内容缓存等。但是Cache的容量有限,因此当Cache的容量用完后,而又有新的内容需要添加进来时, 就需要挑选并舍弃原有的部分内容,从而腾出空间来放新内容。那么将哪部分数据删除这就由LRU来决定,设计一个LRU并不难,但是要想设计一个高效的LRU是很有难度的,这里的高效指的是任何操作的时间复杂度都是o(1), LRU Cache 的替换原则就是将最近最少使用的内容替换掉。其实,LRU译成最久未使用会更形象,因为该算法每次替换掉的就是一段时间内最久没有使用过的内容,那么接下来我们就通过一道题来带着大家模拟实现LRU,题目的内容如下:
在这里插入图片描述
题目给的代码如下:

class LRUCache {
public:
    LRUCache(int capacity) {

    }
    
    int get(int key) {

    }
    
    void put(int key, int value) {

    }
};

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

通过上面的介绍我们知道get函数就是到缓存里面查找数据,如果找到了就返回数据的内容如果没有找到就返回-1,然后put函数有两个功能一个是往容器里面插入数据,另外一个就是更新容器里面的数据,那么要想实现上述功能的话我们可以添加一个哈希表来实现,这样我们查找数据的时间复杂度是o(1),删除数据的时间复杂度也是o(1),更新数据的时间复杂度也是o(1),但是这么做存在一个问题这里的时间复杂度确实符合要求了,但是我们这里要实现的是LRU啊,在删除数据的时候你怎么知道谁的优先级高谁的优先级低呢?简单的说就是你怎么知道要删除的是哪些数据呢?所以单独的一个哈希表肯定是无法满足要求的,那么这时有人说那能不能添加一个优先级队列来辅助实现呢?答案也是不行的,优先级队列确实可以将数据的优先级进行排序,我们一下子就可以知道谁的优先级最高,谁的优先级最低,但是这里有个问题当我们更新数据的时候将优先级调制最高,那你如何将里面的数据优先级队列中的那个元素进行调整呢?即使能够调整那能不能保证时间复杂度是o(1)呢?所以这种方法肯定是不行的那么这个时候有小伙伴又会说能不能添加引用计数的形式来实现呢?答案是可以的但是这么做还是太麻烦了,我们有一个很简单的方法就是添加链表来进行辅助,那么这里的代码如下:

class LRUCache {
public:
    LRUCache(int capacity) {

    }
    
    int get(int key) {

    }
    
    void put(int key, int value) {

    }
private:
    unordered_map<int, int> _hashmap;
    list<pair<int,int>> _l1;
};

查找和新增可以通过哈希表来做到时间复杂度为O(1),在删除的时候可以根据链表中元素的位置来判断优先级的高低,如果对一个元素进行更新的话就将这个链表调整值头部,也就是说越位于链表头部的节点优先级越高,相反位于越位于链表尾部的节点优先级越低,在删除的时候优先删除链表尾部的数据,这么听好像很有道理但是更新的时间复杂度能够做到o(1)吗?好像不行对吧,因为链表不支持随机访问,所以我们得循环遍历从而找到要更新的节点,那么这一步时间复杂度就不是o(1)了更何况后面的步骤,所以我们这里得进行改进,哈希表可以帮助我们在o(1)的时间复杂度找到想要的数据,那要想更新的问题我们就得在哈希表找到数据的同时找到数据对应在链表上的位置,那么这里的做法就是在修改哈希表中记录的元素类型,由原来的int改为list类中的迭代器类型,那么这里的代码就如下:

unordered_map<int, list<pair<int,int>>::iterator> _hashmap;
list<pair<int,int>> _l1;

哈希表中的元素有一个元素记录着链表中相应元素的位置,这样我们就可以使用O(1)的时间找到哈希表中的元素还顺便找到了链表中的元素,构造函数里面提供了一个名为capacity的变量,表示这个容器能够存储数据的个数,那么为了方便我们后面删除数据,这里就再添加一个数据表示当然LRU的容量,并且再使用typedef来重命名依次减轻代码的长度,然后构造函数里面只用对容量变量进行初始化就行那么这里的代码就如下:

class LRUCache {
public:
typedef list<pair<int,int>>::iterator LiIter;
    LRUCache(int capacity) 
        :_capacity(capacity)
    {}
    
    int get(int key) {

    }
    
    void put(int key, int value) {

    }
private:
    unordered_map<int,LiIter> _hashmap;
    list<pair<int,int>> _l1;
    size_t _capacity;
};

get函数非常好实现,首先使用find函数来查找当前的数据在还是不在,那么这里我们可以使用find函数来实现,如果find函数的返回值为哈希表的end的话就说明当前数据不存在,如果不为end的话我们就先通过操作->来得到内部的第二个数据,但是这里的数据依然为一个指向链表的迭代器,所以这里还要通过操作符->来获取第二个数据才是我们想要的,那么这里的代码就下:

int get(int key) {
    auto ret=_hashmap.find(key);
    if(ret!=_hashmap.end())
    {
        return ret->second->second;
    }
    else
    {
        return -1;
    } 
}

但是这里并没有结束,因为我们查找了这里的数据,所以这个数据的优先级应该要被改变,也就是说将元素所在的链表位置进行该表,那么这里有两种方法第一种就是先记录当前的元素,然后将元素进行删除并在链表的头部插入一个相同的数据,最后修改迭代器的指向,但是这种方法太麻烦了我们可以直接使用容器中的splice函数来实现节点的转移,这个函数的参数形式和功能介绍如下:
在这里插入图片描述
在这里插入图片描述
我们可以将自己链表中的节点转移到自己链表的头部位置,那么这里的代码就如下:

int get(int key) {
    auto ret=_hashmap.find(key);
    if(ret!=_hashmap.end())
    {
        LiIter it =ret->second;
        _l1.splice(_l1.begin(),_l1,it);
        return it->second;
    }
    else
    {
        return -1;
    }
}

put函数分为两种情况:一个是新增一个是插入,我们首先来判断一下当前的数据在还是不在如果在的话就是新增,如果不在的话就是插入,那么这里可以通过find函数的返回值来进行判断:

void put(int key, int value) {
    auto ret=_hashmap.find(key);
    if(ret!=_hashmap.end())
    {
        //元素存在那么这里就是更新
    }
    else
    {
        //元素不存在这里是插入
    }
}

如果当前是插入的话这里得进行判断,但是这里判断的时候不能使用list的size函数而是得使用哈希的size函数,因为list中的size是顺序遍历时间复杂度为o(N),而哈希中的size是直接返回内部的数据,所以当对象满了之后我们就得删除数据,首先创建变量记录链表的尾部数据,然后使用哈希的erase函数删除该数据,最后使用pop_back函数删除链表的尾部数据,那么这里的代码如下:

void put(int key, int value) {
   auto ret=_hashmap.find(key);
   if(ret!=_hashmap.end())
   {
       //元素存在那么这里就是更新
   }
   else
   {
       //元素不存在这里是插入
       if(_capacity==_hashmap.size())
       {
           //满了就要删除
           pair<int,int> tmp=_l1.back();
           _l1.pop_back();
           _hashmap.erase(tmp.second);
       }
   }
}

将数据删除之后就往链表的头部插入数据,然后再往哈希表里面插入数据并将该元素的第二个元素初始化为容器开头的位置,那么这里的代码就如下:

void put(int key, int value) {
    auto ret=_hashmap.find(key);
    if(ret!=_hashmap.end())
    {
        //元素存在那么这里就是更新
    }
    else
    {
        //元素不存在这里是插入
        if(_capacity==_hashmap.size())
        {
            //满了就要删除
            pair<int,int> tmp=_l1.back();
            _l1.pop_back();
            _hashmap.erase(tmp.second);
        }
        _l1.push_front(make_pair(key,value));
        _hashmap[key]=_l1.begin();
    }
}

如果当前的对象没有满的话我们就要更新value的值和链表中当前元素所在的位置,那么这里的思路和前面的一致,只不过得修改一下存储的value即可,那么这里的代码如下:

    void put(int key, int value) {
        auto ret=_hashmap.find(key);
        if(ret!=_hashmap.end())
        {
            //元素存在那么这里就是更新
             LiIter it =ret->second;
             it->second=value;
            _l1.splice(_l1.begin(),_l1,it);
        }
        else
        {
            //元素不存在这里是插入
            if(_capacity==_hashmap.size())
            {
                //满了就要删除
                pair<int,int> tmp=_l1.back();
                _l1.pop_back();
                _hashmap.erase(tmp.second);
            }
            _l1.push_front(make_pair(key,value));
            _hashmap[key]=_l1.begin();
        }
    }

写到这里我们的代码就完成了,那么完整的代码如下:

class LRUCache {
public:
typedef list<pair<int,int>>::iterator LiIter;
    LRUCache(int capacity) 
        :_capacity(capacity)
    {}
    
    int get(int key) {
        auto ret=_hashmap.find(key);
        if(ret!=_hashmap.end())
        {
            LiIter it =ret->second;
            _l1.splice(_l1.begin(),_l1,it);
            return it->second;
        }
        else
        {
            return -1;
        }
    }
    
    void put(int key, int value) {
        auto ret=_hashmap.find(key);
        if(ret!=_hashmap.end())
        {
            //元素存在那么这里就是更新
             LiIter it =ret->second;
             it->second=value;
            _l1.splice(_l1.begin(),_l1,it);
        }
        else
        {
            //元素不存在这里是插入
            if(_capacity==_hashmap.size())
            {
                //满了就要删除
                pair<int,int> tmp=_l1.back();
                _l1.pop_back();
                _hashmap.erase(tmp.first);
            }
            _l1.push_front(make_pair(key,value));
            _hashmap[key]=_l1.begin();
        }
    }
private:
    unordered_map<int,LiIter> _hashmap;
    list<pair<int,int>> _l1;
    size_t _capacity;
};

题目测试的结果如下:
在这里插入图片描述
那么这就说明我们的代码没有问题,本篇文章到此结束。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【数据仓库】Apache Hive初体验

为什么使用Hive&#xff1f; 使用Hadoop MapReduce直接处理数据所面临的问题&#xff1a; 人员学习成本太高需要掌握ava语言MapReduce实现&#xff0c;复杂查询逻辑开发难度太大&#xff01; 1&#xff0c;使用Hive处理数据的好处操作接口采用类SQL语法&#xff0c;提供快速开发…

Vue学习Day3——生命周期\组件化

一、Vue生命周期 Vue生命周期&#xff1a;就是一个Vue实例从创建 到 销毁 的整个过程。 生命周期四个阶段&#xff1a;① 创建 ② 挂载 ③ 更新 ④ 销毁 1.创建阶段&#xff1a;创建响应式数据 2.挂载阶段&#xff1a;渲染模板 3.更新阶段&#xff1a;修改数据&#xff0c;更…

C#多线程

C#多线程 C#多线程是C#学习中必不可少的知识&#xff0c;在实际开发中也能有效的提升用户体验&#xff0c;和程序性能。 文章目录 C#多线程前言一、什么是线程、什么是进程、什么是协程&#xff1f;协程优点缺点 线程优点缺点&#xff1a; 进程优点缺点&#xff1a; 二、C# 中…

FANUC机器人SRVO-217故障报警原因分析及参考解决办法

FANUC机器人SRVO-217故障报警原因分析及参考解决办法 如下图所示,示教器提示:SRVO-217紧急停止电路板未找到, 查阅手册可以看到以下的报警说明: 故障原因: 通电时未能识别紧急停止电路板或者增设的安全I/O装置。连接有多个安全I/O装置的系统中,在报警信息的最后,会显示发…

Redis实战(3)——缓存模型与缓存更新策略

1 什么是缓存? 缓存就是数据交换的缓冲区&#xff0c; 是存贮数据的临时区&#xff0c;一般读写性能较高 \textcolor{red}{是存贮数据的临时区&#xff0c;一般读写性能较高} 是存贮数据的临时区&#xff0c;一般读写性能较高。缓存可在多个场景下使用 以一次 w e b 请求为例…

如何少走弯路?蚓链助力零售企业实现数字化转型

基于大环境下的数据驱动&#xff0c;创新业务模式成为了后疫情时代下零售企业冲破困局、拓展业务的必然趋势&#xff0c;新零售概念应运而生。新零售结合数字化应用技术为传统零售企业打造线上营销生态链&#xff0c;帮助企业积累数据&#xff0c;盘活数据实现更大营收价值。 …

微信读书:长期投资(阅读摘录)

微信读书&#xff1a;长期投资&#xff08;阅读摘录&#xff09; 所有投资高手的时间精力都投向了这三大块&#xff1a;行动、思考、读书。 我们把耐心发挥到了极致&#xff0c;这正是价值投资的关键特征之一。 通常在牛市中想要跑赢大盘&#xff0c;难度非常大。 实际上&am…

QTday2信号和槽

点击登录按钮,关闭Widget登录窗口,打开QQList窗口 widget.cpp #include "widget.h"void my_setupUI(Widget *w);Widget::Widget(QWidget *parent): QWidget(parent) {my_setupUI(this); }Widget::~Widget() { }void Widget::login_slots() {//fixemit jump_signal(…

飞行动力学 - 第15节-part 1-操纵力与铰链力矩 之 基础点摘要

飞行动力学 - 第15节-part 1-操纵力与铰链力矩 之 基础点摘要 1. HOTAS全拼2. 操纵杆力&铰链力矩3. 铰链力矩4. 气动补偿&#xff08;Aerodynamic Balancing&#xff09;5. 参考资料 1. HOTAS全拼 Hands On Throttle And Stick 2. 操纵杆力&铰链力矩 操纵杆力&#…

Vue2 第三节 数据代理和事件处理

1.Object.defineProperty 方法 2.数据代理 3.Vue中的数据代理 4.事件的基本使用 5.事件修饰符 6.键盘事件 一.Object.defineProperty 方法 &#xff08;1&#xff09;学习Object.defineProperty为下一节数据代理做准备 &#xff08;2&#xff09;更加高级的给对象添加属…

【LeetCode】383. 赎金信

题目&#xff1a;383. 赎金信 由于此题只含有小写字母,并且magazine里面的字母不可重复使用. 故首先用一个长度为26的整形数组记录magazine里字母出现的次数。 再用这个整形数组跟ransomeNote进行遍历比较&#xff0c;当数组中出现-1时&#xff0c;说明false,否则true. 代码&am…

智慧园区安保人员巡更巡检解决方案,蓝牙信标主动式蓝牙定位导航系统

一、需求分析 目前&#xff0c;大部分写字楼&#xff0c;工厂&#xff0c;学校&#xff0c;银行&#xff0c;车站等场景对安保人员的管理依然靠手填单子记录作业情况&#xff0c;在缺乏信息化手段的情况下&#xff0c;靠人员自觉性或者RFID巡更棒&#xff0c;在这些传统方式下…

el-table数据处理

在写表格时遇到&#xff0c;后端返回的数据是对象&#xff0c;并且缺少字段 1.每一条数据加上 一个字段 2.将对象转成数组 以下是数据 {"groupA": {"groupName": null,"orgName": null,"orgId": null,"allPeoper": &quo…

libcomposite: Unknown symbol config_group_init (err 0)

加载libcomposite.ko 失败 问题描述 如图&#xff0c;在做USB OTG 设备模式的时候需要用到libcomposite.ko驱动&#xff0c;加载失败了。 原因&解决方法 有一个依赖叫configfs.ko的驱动没有安装。可以从内核代码的fs/configfs/configfs.ko中找到这个驱动。先加载confi…

从零开始理解Linux中断架构(23)中断运行临界区和占先调度

Linux在内核中定义了6种运行临界区。 in_interrupt in_interrupt在驱动中使用频率最高的函数了,in_interrupt()就是指示Core是否正在中断处理中,包含了硬中断,软中断运行临界区。如果在中断处理中,则不能调用__do_softirq执行软中断处理。硬中断中不可调度不可中断,所有…

【NLP】温和解读:transformer的核心思想

变压器模型及其关键组件的概述。 一、介绍 在这篇博文中&#xff0c;我将讨论本世纪最具革命性的论文“注意力是你所需要的一切”&#xff08;Vaswani et al.&#xff09;。首先&#xff0c;我将介绍自我注意机制&#xff0c;然后介绍变形金刚的架构细节。在之前的博客文章《从…

STM32 SPI学习

SPI 串行外设设备接口&#xff08;Serial Peripheral Interface&#xff09;&#xff0c;是一种高速的&#xff0c;全双工&#xff0c;同步的通信总线。 SCK时钟信号由主机发出。 SPI接口主要应用在存储芯片。 SPI相关引脚&#xff1a;MOSI&#xff08;输出数据线&#xff…

Lambda表达式常见的Local variable must be final or effectively final原因及解决办法

目录 Local variable must be final or effectively final错误原因 解决办法按照要求定义为final&#xff08;不符合实情&#xff0c;很多时候是查库获取的变量值&#xff09;使用原子类存储变量&#xff0c;保证一致性AtomicReference常用原子类 其它 Local variable must be …

JavaScript中的this指向及绑定规则

在JavaScript中&#xff0c;this是一个特殊的关键字&#xff0c;用于表示函数执行的上下文对象&#xff0c;也就是当前函数被调用时所在的对象。由于JavaScript的函数调用方式多种多样&#xff0c;this的指向也因此而变化。本文将介绍JavaScript中this的指向及绑定规则&#xf…

高速数据采集专家-FMC140【产品手册】

FMC140是一款具有缓冲模拟输入的低功耗、12位、双通道&#xff08;5.2GSPS/通道&#xff09;、单通道10.4GSPS、射频采样ADC模块&#xff0c;该板卡为FMC标准&#xff0c;符合VITA57.1规范&#xff0c;该模块可以作为一个理想的IO单元耦合至FPGA前端&#xff0c;8通道的JESD204…