LRU Cache(C++)

目录

1.什么是LRU Cache

2.LRU Cache的实现

3.LRU Cache的OJ

代码实现


1.什么是LRU Cache

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

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

2.LRU Cache的实现

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

我们通过一道OJ题来详细讲解它的实现。

3.LRU Cache的OJ

146. LRU 缓存 - 力扣(LeetCode)

这道题目的意思就是LRU里面有capacity表示容量,超过容量我们就要进行删除不是最近使用的数据,我们还要实现get和put数据的操作时间复杂度都为O(1),从put操作中我们也能看出它是key-value模型。

代码实现

class LRUCache {
public:
    LRUCache(int capacity) 
    :_capacity(capacity)
    {}
    
    int get(int key) {
        auto it = hashMap.find(key);
        if(it!=hashMap.end())
        {
            LRUlist.splice(LRUlist.begin(),LRUlist,it->second);
            return it->second->second;
        }
        return -1;
    }
    
    void put(int key, int value) {
        auto it = hashMap.find(key);
        if(it==hashMap.end())
        {
            if(_capacity==hashMap.size())
            {
                pair<int,int> back = LRUlist.back();
                hashMap.erase(back.first);
                LRUlist.pop_back();
            }
            LRUlist.push_front(make_pair(key,value));
            hashMap[key]=LRUlist.begin();
        }
        else
        {
            hashMap[key]->second=value;
            LRUlist.splice(LRUlist.begin(),LRUlist,it->second);
        }
    }
    typedef list<pair<int,int>>::iterator Llter;
    private:
    unordered_map<int,Llter> hashMap;
    list<pair<int,int>> LRUlist;
    size_t _capacity;
};

我们从题目中能看出它只要我们实现3种函数,我们就一个一个来讲。

        在讲函数前我们得先有成员变量,从代码这里我们能看到有3个成员变量,分别是哈希表,双向链表和容量。我们之前分析过LRU实际上是key-value模型,所以我们的list容器用的模板类型是pair好理解,但是为什么我们哈希表中的value类型是迭代器而不是节点类型呢?这是因为你的value如果是节点类型确实查和放的过程是O(1),但是你不只是查和放啊,你同时还得更新啊,我们得把最近使用的数据更新到链表的头部啊,这个时候我们的迭代器不仅能查还方便直接改动list的数据。

        说完成员变量之后我们再来讲3个函数,首先是带参构造,带参构造很简单,它形参只给我们了一个,我们就只需要将容量初始化就可以了。我们再来讲解get操作,这个操作也比较简单,它的形参给我们一个key,那么我们直接就用这个key去哈希表里面找,找到了就返回值,找不到就返回-1,同时我们还要更新,我们可以使用splice函数。

splice函数是list的一个成员函数,它可以帮助我们迭代器指向的内容进行位置交换,position是你想指向的位置,x是你想用哪个list对象,i是你想交换的迭代器。我们就可以用它将我们刚刚查到的数据放到begin(也就是头的位置)。这个函数跟你自己去调指针的指向效果是一样的。

        接下来我们再来讲解比较复杂的put操作。按照题目要求我们先来判断一下这个值在不在,在我们就修改值(同时还要将这个刚修改的值的位置调到头部),不在我们就插入,插入前我们需要判断一下它的空间是否需要释放,如果需要我们先将哈希表中的映射关系给去除,再将链表的最后一个数据删除,删除完后我们就可以将数据插入了。

        这样我们简易的LRU模型就出来了,搞懂后其实就没有那么高深了。

<think>嗯,用户让我介绍一下在面试中如何写出C++LRU缓存。首先,我得回忆一下LRU缓存的基本概念。LRU是Least Recently Used的缩写,也就是最近最少使用算法。这种缓存机制在容量达到上限时,会淘汰最久未被访问的数据。那在面试中,面试官可能希望看到候选人能够结合数据结构和算法来实现这个功能。 接下来,我需要考虑如何用C++来实现LRU缓存。通常,LRU缓存的实现需要快速查找和插入,同时还要维护元素的访问顺序。常见的方法是使用哈希表(unordered_map)和双向链表。哈希表提供O(1)的查找时间,而双向链表可以维护元素的访问顺序,最近访问的放在链表头部,最久未访问的在尾部。当需要淘汰数据时,直接删除链表尾部的元素即可。 那么具体步骤应该是怎样的呢?首先,定义一个双向链表的节点结构,包含键、值和前后指针。然后,LRU缓存类需要维护这个链表的头尾节点,以及哈希表。还需要有容量大小,当缓存达到容量时进行淘汰。 接下来,考虑get操作。当获取一个键时,如果存在,需要将该节点移动到链表头部,表示最近访问过,并返回值。如果不存在,返回-1。而put操作则要考虑键是否存在。如果存在,更新值并移动到头部。如果不存在,创建新节点并添加到头部,同时检查容量,若超过则删除尾部节点,并在哈希表中移除对应的键。 这时候,可能需要注意的问题包括如何高效地移动节点到头部,以及如何维护哈希表和链表的同步。比如,在删除尾部节点时,如何快速获取对应的键,以便从哈希表中删除。这需要在节点结构中保存键的信息。 另外,双向链表的实现细节也很重要。比如,添加虚拟头节点和尾节点,可以简化插入和删除操作,避免处理空指针的情况。例如,初始化时,head和tail互为前后节点,这样在添加新节点时,可以方便地在head之后插入,而删除节点时,直接操作前后指针即可。 在面试中,除了写出正确的代码,可能还需要解释选择这些数据结构的原因,比如哈希表保证快速查找,双向链表维护顺序,而单向链表可能在删除节点时需要遍历,导致时间复杂度增加。同时,时间复杂度方面,get和put操作都是O(1)的,这符合LRU缓存的需求。 还要考虑异常处理,比如当容量设置为0或负数时的处理,但通常在题目中假设容量是正整数。此外,代码的可读性和模块化也很重要,比如将节点结构定义为内部结构,封装私有方法如addToHead、removeNode、moveToHead等,提高代码的复用性。 可能的错误点包括在移动节点时没有正确更新前后节点的指针,或者在删除节点时没有同时更新哈希表。另外,在C++中需要手动管理内存,所以在删除节点时,不仅要将其从链表中移除,还要释放对应的内存,避免内存泄漏。 总结一下,实现步骤大致如下: 1. 定义节点结构,包含键、值、前后指针。 2. 初始化LRU缓存时,设置容量,并创建虚拟头尾节点。 3. 实现get方法:查找哈希表,存在则移动节点到头部,返回结果。 4. 实现put方法:存在则更新并移动,不存在则创建新节点,检查容量,必要时删除尾部节点。 5. 编写辅助函数,如添加节点到头部,移除节点,移动节点到头部,删除尾部节点等。 最后,在面试中,可能还需要测试代码的正确性,比如给出测试用例,或者解释代码的运行过程。例如,插入几个元素,触发淘汰机制,验证最近访问的节点是否被正确维护。</think>在面试中实现 LRU(Least Recently Used)缓存是一个常见问题,主要考察对数据结构和算法的综合应用能力。以下是用 C++ 实现 LRU 缓存的详细思路和代码示例: --- ### **核心思路** 1. **数据结构选择**: - **双向链表**:维护缓存项的访问顺序,最近访问的节点放在链表头部,淘汰时删除尾部节点。 - **哈希表(`unordered_map`)**:以键值对形式存储数据,实现 O(1) 时间的快速查找。 2. **关键操作**: - `get(key)`:若存在,将节点移到链表头部,表示最近使用。 - `put(key, value)`:若存在则更新并移动;否则插入新节点,若超出容量则删除尾部节点。 --- ### **C++ 代码实现** ```cpp #include <unordered_map> using namespace std; // 双向链表节点定义 struct DLinkedNode { int key, value; DLinkedNode* prev; DLinkedNode* next; DLinkedNode() : key(0), value(0), prev(nullptr), next(nullptr) {} DLinkedNode(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {} }; class LRUCache { private: unordered_map<int, DLinkedNode*> cache; // 哈希表存储键和节点指针 DLinkedNode* head; // 虚拟头节点(辅助操作) DLinkedNode* tail; // 虚拟尾节点 int capacity; int size; // 将节点添加到链表头部(表示最近使用) void addToHead(DLinkedNode* node) { node->prev = head; node->next = head->next; head->next->prev = node; head->next = node; } // 删除节点 void removeNode(DLinkedNode* node) { node->prev->next = node->next; node->next->prev = node->prev; } // 移动节点到头部 void moveToHead(DLinkedNode* node) { removeNode(node); addToHead(node); } // 删除尾部节点(淘汰最久未使用) DLinkedNode* removeTail() { DLinkedNode* node = tail->prev; removeNode(node); return node; } 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; // 不存在返回-1 DLinkedNode* node = cache[key]; moveToHead(node); // 移动到头部表示最近使用 return node->value; } void put(int key, int value) { if (cache.count(key)) { // 存在则更新 DLinkedNode* node = cache[key]; node->value = value; moveToHead(node); } else { // 不存在则插入新节点 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--; } } } }; ``` --- ### **代码解析** 1. **双向链表操作**: - `addToHead()`:将新节点插入到虚拟头节点之后。 - `removeNode()`:通过调整前后指针删除节点。 - `moveToHead()`:组合删除和插入操作,实现“最近使用”标记。 2. **哈希表同步**: - 插入新节点时,哈希表记录键和节点指针。 - 淘汰节点时,需同时从链表和哈希表中删除。 3. **时间复杂度**: - `get` 和 `put` 操作均为 **O(1)**,满足高效缓存需求。 --- ### **面试要点** 1. **为什么选择双向链表?** - 删除任意节点需要前驱指针,单向链表无法在 O(1) 时间完成。 2. **哈希表的作用?** - 快速定位节点在链表中的位置,避免遍历。 3. **如何保证线程安全?** - 实际工程中需加锁,但面试中通常无需考虑。 掌握上述实现和原理,能在面试中清晰展示对 LRU 机制的理解和编码能力。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值