【数据结构与算法】LRU Cache

简介: 【数据结构与算法】LRU Cache

👉LRU Cache👈


什么是LRU Cache


LRU 是 Least Recently Used 的缩写,意思是最近最少使用,它是一种 Cache 替换算法。 什么是Cache?狭义的 Cache 指的是位于 CPU 和主存间的快速 RAM(随机存取存储器), 通常它不像系统主存那样使用 DRAM 技术,而使用昂贵但较快速的 SRAM 技术。 广义上的Cache 指的是位于速度相差较大的两种硬件之间, 用于协调两者数据传输速度差异的结构。除了 CPU与 主存之间有 Cache, 内存与硬盘之间也有 Cache,乃至在硬盘与网络之间也有某种意义上 Cache,称为 Internet 临时文件夹或网络内容缓存等。

2600fabb68974cdca088693779948028.png

6a72aa641ada483a998169afc9a6476a.png

Cache 的容量有限,因此当 Cache 的容量用完后,而又有新的内容需要添加进来时, 就需要挑选并舍弃原有的部分内容,从而腾出空间来放新内容。LRU Cache 的替换原则就是将最近最少使用的内容替换掉。其实,LRU 译成最久未使用会更形象, 因为该算法每次替换掉的就是一段时间内最久没有使用过的内容。


LRU Cache的实现


实现 LRU Cache 的方法和思路很多,但是要保持高效实现O(1) 的 put 和 get,那么使用双向链表和哈希表的搭配是最高效和经典的。使用双向链表是因为双向链表可以实现任意位置 O(1) 的插入和删除,使用哈希表是因为哈希表的增删查改也是 O(1)。

c6e65116543f4194ba2b0dcb9e62bd8b.png


LRU 缓存

dcf091984c6f486daa2738dd95a50505.png

splice 函数可以转移链表中的节点

5b37749b973c40c3b8156890b298e848.png


哈希表能够保证查找和更新 key 值对应的 value 的时间复杂度为 O(1),也就是能保证 get。那关键点是如何保证 LRU 呢?如果 key 值在哈希表中,那么就 key 值就是最经常访问的值,需要将它放到双向链表的头部(假设双向链表尾部的数据是最近最少用的数据)。因为我们并不知道 key 值在双向链表中的位置,那就无法做到 O(1) 的时间复杂度将 key 值放到链表的头部。那如何解决呢?我们做到的是找到 key 值,就能够找到 key 值在双向链表中的位置。那么我们可以让哈希表存的是 key 值和 key 值在双向链表中的位置(迭代器),双向链表存储是键值对pair<key, value>,这样就能够保证 LRU Cache 了(通过 splice 来调整 key 值在链表中的位置)。

30da6ba4dff64dada676915ecf72cbbe.png

class LRUCache 
{
public:
    LRUCache(int capacity) 
        : _capacity(capacity)
    {}
    int get(int key) 
    {
        auto ret = _hashMap.find(key);
        // 哈希表中有key
        if(ret != _hashMap.end())
        {
            // 更新key的位置
            // 1.erase + push_front 更新迭代器,原迭代器失效
            // 2.splice 将节点转移到头部
            ListIterator it = ret->second;
            _LRUList.splice(_LRUList.begin(), _LRUList, it);
            return it->second; 
        }
        else
            return -1;
    }
    void put(int key, int value) 
    {
        // 1.新增 2.更新
        auto ret = _hashMap.find(key);
        if(ret == _hashMap.end())   // 哈希表中没有key
        {
            // 满了,先删除最近最少用的数据
            if(_capacity == _hashMap.size())
            {
                pair<int, int> LRUData = _LRUList.back();
                _hashMap.erase(LRUData.first);
                _LRUList.pop_back();
            }
            _LRUList.push_front(make_pair(key, value));
            _hashMap[key] = _LRUList.begin();
        }
        else
        {
            // 更新key对应的值
            ListIterator it = ret->second;
            it->second = value;
            // 更新key对应值的位置
            // 1.erase + push_front 更新迭代器,原迭代器失效
            // 2.splice 将节点转移到头部
            _LRUList.splice(_LRUList.begin(), _LRUList, it);
        }
    }
private:
    typedef list<pair<int, int>>::iterator ListIterator;
    // 哈希表做到查找和更新的时间复杂度为O(1)
    // 哈希表中存的是key和key在_LRUList中的位置(迭代器)
    unordered_map<int, ListIterator> _hashMap;
    // LRU假设链表尾部的数据是最近最少用的数据
    // _LRUList中存储的是键值对pair
    list<pair<int, int>> _LRUList;
    size_t _capacity;   // 容量
};

408ac4789b7c42899ffd70b4a7434867.png

👉总结👈


本篇博客主要讲解了什么是 LRU Cache 以及如何设计 LRU Cache等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️


相关文章
|
缓存 算法 内存技术
【高阶数据结构】LRU Cache -- 详解
【高阶数据结构】LRU Cache -- 详解
|
存储 缓存 算法
【数据结构】——LRU Cache
LRU缓存的原理及实现
|
存储 缓存 NoSQL
漫谈 LevelDB 数据结构(三):LRU 缓存( LRUCache)
漫谈 LevelDB 数据结构(三):LRU 缓存( LRUCache)
566 0
漫谈 LevelDB 数据结构(三):LRU 缓存( LRUCache)
|
存储 缓存 算法
|
存储 缓存 算法
【设计数据结构】实现一个 LRUCache
【设计数据结构】实现一个 LRUCache
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
999 9
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
270 59
|
5月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
105 0
栈区的非法访问导致的死循环(x64)
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。
|
10月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
470 77