高级数据结构与算法 | LFU缓存机制(Least Frequently Used)


在之前我写过一篇LRU的博客,如果不了解的建议先看看这篇
高级数据结构与算法 | LRU缓存机制(Least Recently Used)

LFUCache

LFU(Least Frequently Used),即最不经常使用页置换算法。

如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰,这就是LFU的核心思想。

如果我们想要设计一个LFU缓存算法,就需要满足以下功能

  • 初始化时设置缓存大小,当缓存已满后插入元素时需要淘汰现有元素来置换新元素
  • get(key):如果缓存中存在key,则获取value,并更新其使用次数
  • put(key, value) :如果key已经存在,则更新key所对应的value。如果不存在则插入键值对,但是插入时如果缓存已满,则需要将最不常用的键值对淘汰,置换新数据。如果存在多个使用频率相同的键值对,应当选取最久没有使用的。
  • 保证get和put的操作时间复杂度都为O(1)
class LFUCache {
    
    
public:
    LFUCache(int capacity) 
    {
    
    }
    
    int get(int key) 
    {
    
    }
    
    void put(int key, int value) 
    {
    
    }
};

根据以上条件,我们在上面给出了一个算法的大致框架,下面就结合框架以及条件,来设计我们具体的一个结构


结构设计

由于LFU需要根据使用次数来进行缓存淘汰的判断,因此我们每一项中不仅需要存储key,value,还需要存储使用次数freq

每一项的节点设计如下

class LFUNode
{
    
    
public:
    LFUNode(int key, int value, int freq)
        : _key(key)
        , _value(value)
        , _freq(freq)
    {
    
    }

    int _key;   
    int _value;
    int _freq;  //访问次数
};

在上面列的几个条件中,其中最难处理的地方其实就是O(1)的get和put,因为没有任何一个独立的数据结构能够满足这个条件,因此我们需要组合使用多个结构。

如果要保证put中的插入和删除操作达到O(1),那么我们只能选择使用链表来记录我们的缓存信息,但是这次与之前的LRU并不相同,我们在保证使用次数最少的同时,还需要保证淘汰的时同次数中最久没有使用的,这就意味这我们需要根据不同的使用次数,来维护多个链表,并且每个链表中按照使用时间进行排序。

上面的设计依旧存在着一些问题,因为我们在对一个元素进行操作后,它的使用次数就会发生变化,那么我们就需要将其从原先的链表中移动到下一个链表,那么我们如何能够保证O(1)的查找到它,并对他进行操作呢?同时,我们如何能够快速的在多个链表中,查找到它所在的那个链表,以及它即将要去的链表呢?因此,我们还需要结合其他的数据结构来完成这个问题

要保证O(1)的查找链表,这时候就需要用到哈希结构了,我们可以将使用次数freq与每个保存该freq节点的链表建立起映射,保证O(1)的获取链表。

如果要保证O(1)的从指定链表中找到元素,并对其进行操作,我们可以将key与链表中对应元素的迭代器建立起映射,当我们通过key找到迭代器的时候,就可以利用迭代器对链表进行操作。

因此,最终LFUCache的结构设计如下

class LFUCache {
    
    
public:
    LFUCache(int capacity) 
        : _capacity(capacity)
        , _minFreq(0)
    {
    
    }
    
    int get(int key) 
    {
    
    }
    
    void put(int key, int value) 
    {
    
    }
    
private:
    unordered_map<int, list<LFUNode>::iterator> _keyTable;  //建立key与节点迭代器的映射
    unordered_map<int, list<LFUNode>> _freqTable;           //建立freq与该值链表的映射
    int _minFreq;   //保存当前最小的freq,便于淘汰操作
    int _capacity;  //缓存最大容量
};

LFUCache的实现

接下来就该实现get和put功能了,我先列出这两个功能实现的核心思路

get

  • 根据keyTable查找到节点的迭代器,不存在则直接返回
  • 更新使用次数,通过迭代器将节点从原链表中删除,并通过freqTable获取下一个链表,将其头插进链表中,保证最后使用的在最前面,使得链表按照使用时间倒序排序
  • 将新链表中该节点的迭代器更新到keyTable中

put

  • 根据keyTable查找到节点的迭代器,存在则说明是更新,不存在则说明是淘汰
  • 如果是更新,那么操作的内容和上面的get大体相同
  • 如果是插入,需要判断当前缓存是否已满,是否需要淘汰,如果未满则直接插入freq=1的链表中,并将迭代器插入至keyTable中
  • 如果缓存已满,则需要根据minFreq在freqTable中找到对应的最小freq链表,并删除队尾,即最久没有使用的元素,同时在keyTable中淘汰该元素

这里我就将每一步的思路写在下面代码的注释中

完整的实现代码如下

class LFUNode
{
    
    
public:
    LFUNode(int key, int value, int freq)
        : _key(key)
        , _value(value)
        , _freq(freq)
    {
    
    }

    int _key;   
    int _value;
    int _freq;  //访问次数
};

class LFUCache {
    
    
public:
    LFUCache(int capacity) 
        : _capacity(capacity)
        , _minFreq(0)
    {
    
    }
    
    int get(int key) 
    {
    
    
        if(_capacity == 0)
        {
    
    
            return -1;
        }

        auto it = _keyTable.find(key);  //查找到对应的节点
        if(it != _keyTable.end())
        {
    
    
            list<LFUNode>::iterator node = it->second;         //记录节点的value以及freq
            int value = node->_value, freq = node->_freq;

            _freqTable[freq].erase(node); //从freq表中的对应链表删除当前节点
            if(_freqTable[freq].empty() && _minFreq == freq)  //如果删除后链表为空,则判断是否需要更新最小freq
            {
    
    
                _minFreq++;
            }

            _freqTable[freq + 1].push_front(LFUNode(key, value, freq + 1)); //将更新后的节点头插入下一个freq的链表中,保证按照插入时间排序
            it->second = _freqTable[freq + 1].begin();  //更新迭代器

            return value;
        }
        //查找不到直接返回-1
        else
        {
    
    
            return -1;
        }
    }
    
    void put(int key, int value) 
    {
    
    
        if(_capacity == 0)
        {
    
    
            return;
        }

        auto it = _keyTable.find(key);  //查找key是否存在,判断是更新还是插入
        if(it != _keyTable.end())   //如果存在,则是更新
        {
    
    
            list<LFUNode>::iterator node = it->second;
            int freq = node->_freq;
            
            _freqTable[freq].erase(node);
            if(_freqTable[freq].empty() && _minFreq == freq)
            {
    
    
                _minFreq++;
            }

            _freqTable[freq + 1].push_front(LFUNode(key, value, freq + 1));
            it->second = _freqTable[freq + 1].begin();
        }
        else    //不存在则是插入
        {
    
    
            //如果缓存满了,则要先删除后才能插入
            if(_keyTable.size() == _capacity)
            {
    
    
                //查询freq表,获取freq最少且最久没有使用的节点,即freq链表的尾部节点
                LFUNode node = _freqTable[_minFreq].back();
                _keyTable.erase(node._key);        //删除key表中的节点
                _freqTable[node._freq].pop_back();  //删除freq表中的节点
            }

            _freqTable[1].push_front(LFUNode(key, value, 1));          //在freq表中插入新节点
            _keyTable.insert(make_pair(key, _freqTable[1].begin()));//在key表中插入新节点
            _minFreq = 1;   //插入新元素,它为最小的
        }
    }
private:
    unordered_map<int, list<LFUNode>::iterator> _keyTable;  //建立key与节点迭代器的映射
    unordered_map<int, list<LFUNode>> _freqTable;           //建立freq与该值链表的映射
    int _minFreq;   //保存当前最小的freq,便于淘汰
    int _capacity;  //缓存最大容量
};

猜你喜欢

转载自blog.csdn.net/qq_35423154/article/details/109826279