自学内容网 自学内容网

LRU Cache

一、什么是LRU Cache

LRU是Least Recently Used的缩写,意思是最近最少使用,它是一种Cache替换算法。

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

在这里插入图片描述

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

二、LRU Cache的实现

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

在这里插入图片描述

我们以LeetCode上的一道题来实现LRU Cache

LeetCode 146.LRU缓存

这道题要求使用O(1)的时间复杂度来完成get和put的操作,我们最先想到的是使用哈希表,这样查找的时间复杂度就是O(1),我们还需要一个链表来保存节点,如果链表保存的是key和val,完成数据的更新操作,即将链表中key的位置移到头部,更新顺序,但是更新的时候需要遍历查找key,此时的时间复杂度为O(N),就不能满足O(1)的要求,我们需要找到key,就要找到key对应存储数据在链表中的位置,此时我们可以将哈希表的value改为链表的迭代器,这样找到key,就可以得到链表的迭代器,就可以完成更新操作

第一种方式我们自己实现一个链表

struct Node
{
    int key;
    int val;
    Node* prev;
    Node* next;
    Node():key(0),val(0),prev(nullptr),next(nullptr){}
    Node(int _key,int _val):key(_key),val(_val),prev(nullptr),next(nullptr){}
};
class LRUCache
{
private:
    unordered_map<int,Node*> cache;
    Node* head;
    Node* tail;
    int size;
    int capacity;
public:
    LRUCache(int _capacity)
    :capacity(_capacity),size(0)
    {
        // 使用伪头部和伪尾部节点
        head = new Node();
        tail = new Node();
        head->next = tail;
        tail->prev = head;
    }
    
    int get(int key)
    {   
        if(!cache.count(key)) return -1;
        // 如果 key 存在,先通过哈希表定位,再移到头部
        Node* node = cache[key];
        moveToHead(node);
        return node->val;
    }
    
    void put(int key, int value)
    {
        // 如果 key 不存在,创建一个新的节点
        if(!cache.count(key))
        {
            Node* node = new Node(key,value);
            // 添加进哈希表
            cache[key] = node;
            // 添加至双向链表的头部
            addToHead(node);
            ++size;
            if(size > capacity)
            {
                // 如果超出容量,删除双向链表的尾部节点
                Node* removed = removeTail();
                // 删除哈希表中对应的项
                cache.erase(removed->key);
                delete removed;
                --size;
            }
        }
        else
        {
            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            Node* node = cache[key];
            node->val = value;
            moveToHead(node);
        }
    }

    void addToHead(Node* node)
    {
        node->next = head->next;
        head->next->prev = node;
        node->prev = head;
        head->next = node;
    }

    void removeNode(Node* node)
    {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    void moveToHead(Node* node)
    {
        removeNode(node);
        addToHead(node);
    }

    Node* removeTail()
    {
        Node* node = tail->prev;
        removeNode(node);
        return node;
    }
};

第二种方式使用STL库中的链表

class LRUCache
{
private:
    typedef list<pair<int,int>>::iterator iter;
    list<pair<int,int>> _LRUList;
    unordered_map<int,iter> _hashMap;
    int _capacity;
public:
    LRUCache(int capacity)
    :_capacity(capacity)
    {}
    
    int get(int key)
    {
        auto ret = _hashMap.find(key);
        if(ret == _hashMap.end()) return -1;

        //使用splice函数进行移动节点
        iter it = ret->second;
        _LRUList.splice(_LRUList.begin(),_LRUList,it);
        return it->second;
    }
    
    void put(int key, int value)
    {
        auto ret = _hashMap.find(key);
        if(ret != _hashMap.end())
        {
            // 没有找到就更新节点的值,将其移动到链表的头部
            iter it = ret->second;
            it->second = value;
            
            _LRUList.splice(_LRUList.begin(),_LRUList,it);
        }
        else
        {
            //如果满了就先删除链表的尾部节点
            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();
        }
    }
};

std::list::splice介绍

splice是std::list容器的一个成员函数,用于将另一个std::list中的元素移动到当前list的指定位置。它的语法如下:

cppCopy Codevoid splice (iterator position, list& x);
void splice (iterator position, list& x, iterator it);
void splice (iterator position, list& x, iterator first, iterator last);

参数说明:

  • position:指定插入位置的迭代器。
  • x:另一个std::list容器。
  • it:指向x中某个元素的迭代器,表示要移动的元素。
  • first, last:表示要移动的元素范围,包括first但不包括last。

使用splice函数可以实现以下操作:

  1. 将另一个std::list中的所有元素移动到当前list的指定位置。
  2. 将另一个std::list中的某个元素移动到当前list的指定位置。
  3. 将另一个std::list中指定范围的元素移动到当前list的指定位置。

移动元素后,原来的std::list容器将不再包含这些元素,而被移动的元素将被插入到当前list的指定位置。

注意事项:

  • 使用splice函数时,被移动的元素会保持原来的顺序。
  • 被移动的元素将从原来的list中删除,插入到当前list中。
  • 如果移动的元素与当前list中的元素类型不兼容,会导致编译错误。

原文地址:https://blog.csdn.net/qq_67582098/article/details/142371317

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!