【LeetCode-模拟】LFU缓存

题目描述

请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。它应该支持以下操作:get 和 put。

  • get(key) - 如果键存在于缓存中,则获取键的值(总是正数),否则返回 -1。
  • put(key, value) - 如果键已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量时,则应该在插入新项之前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除最久未使用的键。

「项的使用次数」就是自插入该项以来对其调用 get 和 put 函数的次数之和。使用次数会在对应项被移除后置为 0 。

进阶:
你是否可以在 O(1) 时间复杂度内执行两项操作?

示例:

LFUCache cache = new LFUCache( 2 /* capacity (缓存容量) */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回 1
cache.put(3, 3);    // 去除 key 2
cache.get(2);       // 返回 -1 (未找到key 2)
cache.get(3);       // 返回 3
cache.put(4, 4);    // 去除 key 1
cache.get(1);       // 返回 -1 (未找到 key 1)
cache.get(3);       // 返回 3
cache.get(4);       // 返回 4

题目链接: https://leetcode-cn.com/problems/lfu-cache/

思路

这题和LRU缓存机制很像。
LRU

  • 缓存满时,删除最久未使用的元素;

LFU:

  • 缓存满时,删除使用频次最少的元素;

所以,在 LFU 中,我们还需要记录每个元素被访问的次数,每个元素除了 key,val 之外,还需要定义 freq 表示访问的次数:

struct Node{
    int key;
    int val;
    int freq;

    Node(int key, int val, int freq):key(key), val(val), freq(freq){}
};

我们使用两个哈希表:unordered_map<int, list<Node>::iterator> keyTable 用来存储 key 到链表节点指针的映射;unordered_map<int, list<Node>> freqTable 用来存储频数 freq 到链表的映射,具体如下图:

图来自这篇题解,左边的哈希表为 keyTable,右边的哈希表为 freqTale。可以看到 freqTable 根据频数存储了具体的链表,而 keyTable 存储了 key 到链表节点地址的映射。

除此之外,我们还需要一个变量 minFreq 来存储目前最少访问的次数,通过 freqTable[minFreq].pop_back() 删除使用最少的节点。

算法步骤:

  • get(key):

    • 如果 key 不在 keyTable 中,返回 -1;
    • 否则,通过 keyTable[key] 获取 key 对应的节点地址 it,然后通过 it 得到节点的 key、val、freq,将节点从 freqTable[freq] 对应的链表删除,然后将该节点加入到 freqTable[freq+1] 对应的链表头;
  • put(key, value):

    • 如果 key 在 keyTable 中,则使用和 get(key) 中的第二步类似的方法;
    • 否则,如果缓存已经满了,则根据 minFreq 删除使用最少的节点,然后设置 minFreq 为 1,将新的节点放入 freqTable[1] 对应的链表头。

具体代码如下:

struct Node{
    int key;
    int val;
    int freq;

    Node(int key, int val, int freq):key(key), val(val), freq(freq){}
};

class LFUCache {
private:
    unordered_map<int, list<Node>::iterator> keyTable;
    unordered_map<int, list<Node>> freqTable;
    int capacity;
    int minFreq;
public:
    LFUCache(int capacity) {
        this->capacity = capacity;
        this->minFreq = 0;
    }
    
    int get(int key) {
        if(keyTable.count(key)==0) return -1;
        else{
            auto it = keyTable[key];   // it 为 key 对应的节点地址
            int val = it->val;
            int freq = it->freq;
            freqTable[freq].erase(it);  // 在 freqTable[freq] 对应的链表中删除节点
            if(freqTable[freq].size()==0){  // 如果删除后 freqTable[freq] 为空
                freqTable.erase(freq);
                if(minFreq==freq) minFreq++; // 注意这一步
            }
            freqTable[freq+1].push_front(Node(key, val, freq+1)); // 将节点放入 freqTable[freq+1] 对应的链表中
            keyTable[key] = freqTable[freq+1].begin();
            return val;
        }
    }
    
    void put(int key, int value) {
        if(this->capacity==0) return;   // 注意判断容量是否为 0
        if(keyTable.count(key)!=0){    // key 已经在缓存中了
            auto it = keyTable[key];   // 下面的步骤和 get 函数中 else 部分基本相同
            int freq = it->freq;
            freqTable[freq].erase(it);
            if(freqTable[freq].size()==0){
                freqTable.erase(freq);
                if(minFreq==freq) minFreq++;
            }
            freqTable[freq+1].push_front(Node(key, value, freq+1));
            keyTable[key] = freqTable[freq+1].begin();
        }
        else{                          // key 不在缓存中
            if(keyTable.size()==this->capacity){   // 缓存容量已满
                Node node = freqTable[minFreq].back();   // 通过 minFreq 找到使用最少的节点 back()
                keyTable.erase(node.key);      // 删除使用最少的节点
                freqTable[minFreq].pop_back();
                if(freqTable[minFreq].empty()){
                    freqTable.erase(minFreq);
                }
            }
            freqTable[1].push_front(Node(key, value, 1));
            keyTable[key] = freqTable[1].begin();
            minFreq = 1;   // minFreq 置为 1
        }
    }
};

/**
 * Your LFUCache object will be instantiated and called as such:
 * LFUCache* obj = new LFUCache(capacity);
 * int param_1 = obj->get(key);
 * obj->put(key,value);
 */

注意点:

  • 在 freqTable[freq] 对应的链表中插入节点时,都是插入链表头 (push_front());
  • 每次在 freqTable[freq] 对应的链表中删除节点后,都要判断 freqTable[freq] 是否为空,如果为空,则将 freq 从 freqTable 中删除,并在必要的情况下更新 minFreq。

参考

1、https://leetcode-cn.com/problems/lfu-cache/solution/ha-xi-shuang-xiang-lian-biao-lfuhuan-cun-by-realzz/
2、https://leetcode-cn.com/problems/lfu-cache/solution/ha-xi-biao-shuang-xiang-lian-biao-java-by-liweiwei/

原文地址:https://www.cnblogs.com/flix/p/13680008.html