数据结构:LRU Cache

news2024/10/23 11:29:30

数据结构:LRU Cache

    • LRU Cache
    • 实现
      • 类架构
      • set
      • get
      • 测试
    • 总代码


LRU Cache

cache意为缓存,硬件层面指CPU主存之间的缓存,用于减缓两者之间的速度差距。广义上,可以把cache简单理解为一个临时存储区域。

cache的容量是非常有限的,当cache的容量使用完后,如果还有新的内容添加进来,那么此时就要舍弃掉原有的部分内容,从而腾出空间放新内容。

所谓的LRU cache,全称Latest Recent Used Cache。这种缓存的替换原则,是将最久未使用的数据替换掉,来放新内容。本博客将以C++实现一个LRU Cache

实现LRU Cache的方法非常多,但是要实现最高效的增加与获取数据,也就是getset都是O(1)的时间复杂度,最经典的方案是哈希表 + 双向链表

对于双向链表来说,插入与删除的时间复杂度都是O(1),但是查询的速度是O(N)。因此引入一个哈希表,降低查询的时间复杂度到O(1)

另外的,由于链表是顺序形存储结构,所以其节点的先后也可以表示顺序,所以把最近访问的值放在链表的头部,最久没访问的值放在链表尾部。在LRU Cache满时,直接尾删链表即可,时间复杂度还是O(1)

如图:

在这里插入图片描述

蓝色的是哈希表,红色的是双向链表。在哈希表中,存储key和指向链表的指针。

查询LRU Cache时,通过key计算哈希表的位置,拿到指针,直接访问链表的指定节点了,这个过程时间复杂度为O(1)

插入删除时,也通过key在哈希表中拿到指针,随后进行链表的插入删除操作,该过程时间复杂度也为O(1)


实现

类架构

template <typename K, typename V, typename Hash = hash<K>>
class LRUCache
{
public:
    LRUCache(int capacity)
        : _capacity(capacity)
    {}
    
private:
    unordered_map<K, typename list<pair<K, V>>::iterator, Hash> _hash;
    list<pair<K, V>> _cache;
    int _capacity;
};

模板参数:

  • Kkey的类型
  • Vvalue的类型
  • Hash:哈希函数,如果传入的K是复杂类型,unordered_map可能无法计算哈希值,此时要用户自己传哈希函数

类成员:

  • _hash:存储从key到指向链表指针的映射,C++中直接使用迭代器list<pair<K, V>>::iterator代替指针
  • _cache:缓存本体,存储pair<K, V>键值对
  • _capacity:缓存的最大容量

在构造函数时,用户传入一个capacity,表示该LRU Cache的最大容量。


set

函数声明:

void set(K key, V value)

set值时,key可能存在,也可能不存在,此时就要判断,然后分别处理:

如果key存在:

  1. 更新value的值为新的value
  2. key视为被访问,将链表节点移到链表头部

代码:

auto hash_it = _hash.find(key);

if (hash_it != _hash.end()) // key原先存在
{
    auto list_it = hash_it->second; // 指向链表节点的迭代器
    list_it->second = value; // 更新节点值

    _cache.emplace_front(list_it->first, list_it->second); // 构造一个节点头插到链表
    _cache.erase(list_it); // 删除链表原先的节点
    hash_it->second = _cache.begin(); // 更新哈希表指向迭代器的指针
}

首先通过_hash.find(key)查询key是否在哈希表存在,得到迭代器hash_it,如果hash_it != _hash.end(),说明key原先存在。

hash_it->second通过哈希表拿到指向链表节点的迭代器,list_it->second = value将节点的值更新。

接下来完成节点的移动,把节点移动到链表头部。

为了提高效率,此处直接删除原先的节点,并用原先的数据构造一个新节点头插到链表。

由于此时已经创建了新节点,原先哈希表存储的迭代器就失效了,hash_it->second = _cache.begin();更新哈希表的迭代器。

如果key不存在,那么就要创建新的节点,但是在创建新节点之前,就要考虑LRU Cache是否已经满了?如果满了,要把最久没使用的数据删掉

  1. 如果满了,把最久没使用的数据删掉
  2. 利用keyvalue构造新节点,头插到链表
  3. 哈希表新增key
else
{
    if (_capacity == _hash.size())
    {
        pair<K, V>& back = _cache.back(); // 最久没使用的元素
        _hash.erase(back.first); // 哈希表中删除
        _cache.pop_back(); // 在链表中删除
    }

    _cache.emplace_front(key, value);
    _hash[key] = _cache.begin();
}

首先if (_capacity == _hash.size())判断是否缓存已满,如果满了,取出链表尾部的元素_cache.back(),这就是最久没使用的元素,同时在哈希表和链表中删掉节点。

_cache.emplace_front(key, value)在链表中头插新节点,_hash[key] = _cache.begin()在哈希表中报错新的迭代器与key的映射关系。


get

get也很简单:

  1. 判断key是否存在,不存在直接返回
  2. 将被查询的节点,移动到链表头部
  3. 返回查询的value
V get(K key)
{
    auto hash_it = _hash.find(key);
    if (hash_it == _hash.end())
        return V();

    auto list_it = hash_it->second;
    _cache.emplace_front(list_it->first, list_it->second);
    _cache.erase(list_it);
    hash_it->second = _cache.begin();

    return _cache.begin()->second;
}

首先hash_it == _hash.end()判断是否存在该元素,如果不存在直接返回V()

auto list_it = hash_it->second拿到指向节点的迭代器,与之前一样,在头部构造一个数据一样的新节点,然后把迭代器指向的节点erase掉。此处哈希表的迭代器会失效,hash_it->second = _cache.begin()更新哈希表的迭代器。

最后返回用户查询的value值,由于之前已经把节点移到头部了,所以就是_cache.begin()


测试

写一个print函数,输出当前链表:

void print()
{
    int i = 1;
    for (auto& p : _cache)
    {
        cout << i++ << ": " << p.first << " - " << p.second << endl;
    }
}

测试代码:

int main()
{
    LRUCache<int, string> lru(5);

    cout << "插入五个数:" << endl;
    lru.set(1, "a");
    lru.set(2, "b");
    lru.set(3, "c");
    lru.set(4, "d");
    lru.set(5, "e");
    lru.print();

    cout << "获取key 1:" << endl;
    lru.get(1);
    lru.print();

    cout << "插入第六个数:" << endl;
    lru.set(6, "f");
    lru.print();

    return 0;
}

测试结果:

插入五个数:
1: 5 - e
2: 4 - d
3: 3 - c
4: 2 - b
5: 1 - a
获取key 1:
1: 1 - a
2: 5 - e
3: 4 - d
4: 3 - c
5: 2 - b
插入第六个数:
1: 6 - f
2: 1 - a
3: 5 - e
4: 4 - d
5: 3 - c

总代码

  • LRUCache.hpp
#pragma once
#include <vector>
#include <list>
#include <unordered_map>

using namespace std;

template <typename K, typename V, typename Hash = hash<K>>
class LRUCache
{
public:
    LRUCache(int capacity)
        : _capacity(capacity)
    {}

    void set(K key, V value)
    {
        auto hash_it = _hash.find(key);

        if (hash_it != _hash.end())
        {
            auto list_it = hash_it->second;
            list_it->second = value;

            _cache.emplace_front(list_it->first, list_it->second);
            _cache.erase(list_it);
            hash_it->second = _cache.begin();
        }
        else
        {
            if (_capacity == _hash.size())
            {
                pair<K, V>& back = _cache.back();
                _hash.erase(back.first);
                _cache.pop_back();
            }

            _cache.emplace_front(key, value);
            _hash[key] = _cache.begin();
        }
    }

    V get(K key)
    {
        auto hash_it = _hash.find(key);
        if (hash_it == _hash.end())
            return V();

        auto list_it = hash_it->second;

        _cache.emplace_front(list_it->first, list_it->second);
        _cache.erase(list_it);
        hash_it->second = _cache.begin();

        return _cache.begin()->second;
    }

    void print()
    {
        int i = 1;
        for (auto& p : _cache)
        {
            cout << i++ << ": " << p.first << " - " << p.second << endl;
        }
    }

private:
    unordered_map<K, typename list<pair<K, V>>::iterator, Hash> _hash;
    list<pair<K, V>> _cache;
    int _capacity;
};
  • test.cpp
#include <iostream>

#include "LRUCache.hpp"

int main()
{
    LRUCache<int, string> lru(5);

    cout << "插入五个数:" << endl;
    lru.set(1, "a");
    lru.set(2, "b");
    lru.set(3, "c");
    lru.set(4, "d");
    lru.set(5, "e");
    lru.print();

    cout << "获取key 1:" << endl;
    lru.get(1);
    lru.print();

    cout << "插入第六个数:" << endl;
    lru.set(6, "f");
    lru.print();

    return 0;
}

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

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

相关文章

从0到1掌握大模型

人人都看得懂的大模型简介 大模型就像一座庞大的图书馆&#xff0c;里面有非常多的书籍。但与普通图书馆不同的是&#xff0c;这座图书馆中的每本书都是关于不同事物的描述和知识。而这些书籍中的每一页都代表了这个事物的一些特征或细节。现在&#xff0c;想象一下&#xff0…

前端文件流导出

1、前端代码 ​ /** 导出 */ const handleExport async () > {let config {responseType: blob,headers: {Content-Type: application/json,},};const res await getTargetExport(config);const blob new Blob([res]);const fileName PK目标跟进导出列表.xls;const li…

基于Python绘制一个三角形

一、程序 import turtledef draw_triangle(side_length):# 初始化turtle对象并设置速度my_turtle turtle.Turtle()my_turtle.speed(1)# 绘制三角形for _ in range(3):my_turtle.forward(side_length)my_turtle.left(120)# 结束后关闭窗口防止立即退出turtle.done()# 设定三角…

前端面试经验总结1(简历篇)

本文分为3部分&#xff0c;分别为第一部分简历篇&#xff0c;第二部分经典问题篇以及第三部分知识体系篇&#xff0c;都是个人面试经验及同行面试经验总结和整理。 我对于简历的理解是这样的&#xff0c;简历的作用是让看简历的人能够快速、准确地捕捉到有用信息&#xff1a; 你…

【MATLAB源码-第274期】基于matlab的高阶累积量矩阵和PCA的JADE算法在盲信号分离中的应用仿真,输出源信号,混合信号和分离信号。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 基于高阶累积量矩阵和主成分分析&#xff08;PCA&#xff09;的JADE算法是一种典型的盲源分离算法&#xff0c;在信号处理领域中&#xff0c;广泛应用于分离混合信号的独立源。盲源分离问题的核心在于从观测信号中提取出源信…

【C语言】动态内存管理(下)

本篇博客将讲解以下知识&#xff1a; 1、calloc和realloc 2、常见的动态内存错误 1、calloc和realloc &#xff08;1&#xff09;calloc C语言中还提供了一个函数叫calloc&#xff0c;calloc也用来动态内存分配 calloc函数原型&#xff1a; void* calloc(size_t num, …

基于springboo+vue+mysql私人西服定制设计与实现(源码+定制+开发)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

初始Python篇(2)——逻辑控制、序列

找往期文章包括但不限于本期文章中不懂的知识点&#xff1a; 个人主页&#xff1a;我要学编程(ಥ_ಥ)-CSDN博客 所属专栏&#xff1a; Python 目录 顺序结构 选择结构 循环结构 遍历循环-for break 和 continue 无限循环-while pass空语句 猜数字小游戏 序列 序列相…

如何管理和维护自动化测试

将测试数据、测试脚本和测试结果进行有效的管理和维护是软件测试过程中的重要任务&#xff0c;它直接关系到测试的质量和效率。以下是对这三个方面分别进行管理和维护的具体建议&#xff1a; 一、测试数据的管理和维护 1. 数据收集 来源选择&#xff1a;测试数据主要来源于生…

Redis 简单编写C++客户端

我们之前的学习都是通过Redis自带的命令行式的客户端来使用Redis的&#xff0c;我们在执行命令的时候&#xff0c;都是手动执行的。然而这种操作方式并不是日常开发的主要形式。 更多的时候&#xff0c;是使用Redis的api来实现定制化的Redis客户端程序&#xff0c;进一步来操作…

一场被真话包场的前端面试记录……

​ 面试官&#xff1a;等很久了吧&#xff0c;我是故意的&#xff01;这不是要装的忙一点&#xff0c;让领导看看嘛。前端小李&#xff1a;看你们这公司不大&#xff0c;破事倒是不少&#xff01;要不是到门口了&#xff0c;我都想回去。面试官&#xff1a;没错&#xff0c;我们…

为什么kafka处理速度这么快啊!?

Apache Kafka 是一个开源的分布式事件流平台&#xff0c;具有高吞吐量、可扩展性和持久性等特点。目前被广泛使用&#xff0c;本文将介绍kafka的组成部分、特点&#xff0c;并且解释为什么kafka能够快速处理。 一、基本组成部分 1. 消息&#xff08;Message&#xff09;&#x…

【AAOS】Android Automotive 14模拟器源码下载及编译

源码下载 repo init -u https://android.googlesource.com/platform/manifest -b android-14.0.0_r20 repo sync -c --no-tags --no-clone-bundle 源码编译 source build/envsetup.sh lunch sdk_car_x86_64-trunk_staging-eng make -j8 运行效果 emualtor Home All apps …

Nature 正刊丨镊子时钟的通用量子运算和基于ancilla的读出

01摘要 通过利用纠缠来提高测量精度是量子计量学长期追求的目标1,2。然而&#xff0c;在存在噪声的情况下获得量子理论允许的最佳灵敏度是一个突出的挑战&#xff0c;需要最佳的探测态生成和读出策略3,4,5,6,7。中性原子光学钟8是测量时间的主要系统&#xff0c;在纠缠产生方面…

【C++贪心 分治】1717. 删除子字符串的最大得分|1867

本文涉及知识点 贪心 分治 LeetCode1717. 删除子字符串的最大得分 给你一个字符串 s 和两个整数 x 和 y 。你可以执行下面两种操作任意次。 删除子字符串 “ab” 并得到 x 分。 比方说&#xff0c;从 “cabxbae” 删除 ab &#xff0c;得到 “cxbae” 。 删除子字符串"…

IEEE出版:第十二届信息系统与计算技术国际学术会议参会邀请

第十二届信息系统与计算技术国际会议&#xff08;ISCTech 2024&#xff09;将于2024年11月8-11日在西安举办&#xff0c;聚焦信息系统与计算技术领域&#xff0c;邀请专家学者参与交流。投稿论文将经严格审稿并出版至IEEE Xplore&#xff0c;支持EI等检索。会议涵盖多个主题&am…

编译cifx的驱动代码报错“netlink/cli/utils.h: No such file or directory”

ethercat编译时遇到netlink/cli/utils.h: No such file or directory 确认已安装对应的库&#xff1a; sudo apt-get install libnl-3-devsudo apt-get install libnl-genl-3-dev依旧编译失败&#xff0c;找到文件所在路径。 发现该路径在系统默认的库查找路径内 选择创建软链…

MySQL数据库操作——(2)

3 DML&#xff08;数据操作&#xff09; 3.1 添加数据 insert语法&#xff1a; 向指定字段添加数据 insert into 表名 (字段名1, 字段名2) values (值1, 值2); 全部字段添加数据 insert into 表名 values (值1, 值2, ...); 批量添加数据&#xff08;指定字段&#xff09; ins…

雪花啤酒:以AI数智化重新书写啤酒产业

作者|斗斗 编辑|皮爷 出品|产业家 在中国&#xff0c;雪花啤酒的名字刻在了每个人脑中。就像在泰山南天门&#xff0c;必须拿一瓶“勇闯天涯”拍照打卡一样&#xff0c;已经成为了一个“必选项”。 1993年&#xff0c;华润集团收购了沈阳雪花啤酒厂55%股份&#xff0c;…

挑战高薪!腾讯/华为招聘提到的PMP证书来了!

想必大家伙都知道&#xff1a;想要挑战高薪工作&#xff0c;除了要有过硬的专业能力外&#xff0c;不断进行自我提升也非常重要&#xff01; 现在招聘中&#xff0c;很多与岗位匹配的证书&#xff0c;也被纳入了基本条件中&#xff0c;比如被频繁提到的PMP证书&#xff0c;无论…