leetcode 链接:LFU 缓存

题目

请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。
实现 LFUCache 类:

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

注意「项的使用次数」就是自插入该项以来对其调用 getput 函数的次数之和。使用次数会在对应项被移除后置为 0 。
为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。
当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 getput 操作,使用计数器的值将会递增。

进阶:你可以为这两种操作设计时间复杂度为 O(1) 的实现吗?

示例:

  1. 输入:
  2. ["LFUCache", "put", "put", "get", "put", "get", "get", "put", "get", "get", "get"]
  3. [[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]]
  4. 输出:
  5. [null, null, null, 1, null, -1, 3, null, -1, 3, 4]
  6. 解释:
  7. // cnt(x) = 键 x 的使用计数
  8. // cache=[] 将显示最后一次使用的顺序(最左边的元素是最近的)
  9. LFUCache lFUCache = new LFUCache(2);
  10. lFUCache.put(1, 1); // cache=[1,_], cnt(1)=1
  11. lFUCache.put(2, 2); // cache=[2,1], cnt(2)=1, cnt(1)=1
  12. lFUCache.get(1); // 返回 1
  13. // cache=[1,2], cnt(2)=1, cnt(1)=2
  14. lFUCache.put(3, 3); // 去除键 2 ,因为 cnt(2)=1 ,使用计数最小
  15. // cache=[3,1], cnt(3)=1, cnt(1)=2
  16. lFUCache.get(2); // 返回 -1(未找到)
  17. lFUCache.get(3); // 返回 3
  18. // cache=[3,1], cnt(3)=2, cnt(1)=2
  19. lFUCache.put(4, 4); // 去除键 1 ,1 和 3 的 cnt 相同,但 1 最久未使用
  20. // cache=[4,3], cnt(4)=1, cnt(3)=2
  21. lFUCache.get(1); // 返回 -1(未找到)
  22. lFUCache.get(3); // 返回 3
  23. // cache=[3,4], cnt(4)=1, cnt(3)=3
  24. lFUCache.get(4); // 返回 4
  25. // cache=[3,4], cnt(4)=2, cnt(3)=3

解答 & 代码

参考:LFU五种实现方式,从简单到复杂

LFUCache 构造如下图所示,包含

  • 一个哈希 Map,存储 键值对
  • capacity 容量
  • 两层链表(可以想象为类似哈希表的构造)
    • 外层链表的节点代表各个频度,从头节点到尾节点频度递减
    • 外层链表的每个频度又对应一个链表,代表该频度的节点链表,从头节点到尾节点由新到旧

image.png

class FreqNode;                // 前向声明

class Node {                // 节点类
public:
    int key;                // 键
    int val;                // 值
    Node* pre;                // 指向前一节点(更靠后被使用/更新)的指针
    Node* next;                // 指向后一节点(更靠前被使用/更旧)的指针
    FreqNode* freqNode;        // 对应频度的频度节点指针

    Node()
    {
        pre = nullptr;
        next = nullptr;
        freqNode = nullptr;
    }
    Node(int key, int val)
    {
        this->key = key;
        this->val = val;
        pre = nullptr;
        next = nullptr;
        freqNode = nullptr;
    }
};

class FreqNode {            // 频度链表节点类
public:
    int freq;                // 频度
    Node* preHead;            // 该频度的节点链表的虚拟头节点
    Node* afterEnd;            // 该频度的节点链表的虚拟尾节点
    FreqNode* pre;            // 指向前一个(更大频度)的频度节点的指针
    FreqNode* next;            // 指向后一个(更小频度)的频度节点的指针

    FreqNode(int freq)
    {
        this->freq = freq;
        preHead = new Node(-1, -1);
        afterEnd = new Node(-1, -1);
        preHead->next = afterEnd;
        afterEnd->pre = preHead;
    }

    void removeNode(Node* node)        // 从 node 节点所在频度的链表中移除该节点,但不 delete 该节点
    {
        if(isEmpty())
            return;

        node->pre->next = node->next;
        node->next->pre = node->pre;
    }

    void deleteNode(Node* node)        // 从 node 节点所在频度的链表中删除该节点(delete 该节点)
    {
        if(isEmpty())
            return;
        node->pre->next = node->next;
        node->next->pre = node->pre;
        delete node;
    }

    void insertNode(Node* node)        // 前插,将 node 节点插入当前频度对应链表的头部(虚拟头节点之后)
    {
        Node* next = preHead->next;
        preHead->next = node;
        node->pre = preHead;
        node->next = next;
        next->pre = node;
        node->freqNode = this;
    }

    bool isEmpty()                    // 判断当前频度的节点链表是否为空
    {
        return preHead->next == afterEnd;
    }
};

class LFUCache {
private:
    unordered_map<int, Node*> map;    // 哈希表,存储 key 和 node 的键值对
    FreqNode* dummyHead;            // 频度链表(外层链表)的虚拟头节点
    FreqNode* dummyEnd;                // 频度链表(外层链表)的虚拟尾节点
    int capacity;                    // 容量

    bool isEmpty()                    // 频度链表(外层链表)是否为空
    {
        return dummyHead->next == dummyEnd;
    }

    void deleteFreqNode(FreqNode* freqNode)    // 从外层链表删除频度节点 freqNode
    {
        if(isEmpty())
            return;
        freqNode->pre->next = freqNode->next;
        freqNode->next->pre = freqNode->pre;
        delete freqNode;
    }

    void freqInc(Node* node)                // 将 node 移动到 频度+1 的频度链表
    {
        FreqNode* freqList = node->freqNode;
        freqList->removeNode(node);
        int freq = freqList->freq;
        FreqNode* preFreqList = freqList->pre;
        if(freqList->isEmpty())
            deleteFreqNode(freqList);
        if(preFreqList->freq != freq + 1)
        {
            FreqNode* newFreqList = new FreqNode(freq + 1);
            newFreqList->pre = preFreqList;
            newFreqList->next = preFreqList->next;
            preFreqList->next->pre = newFreqList;
            preFreqList->next = newFreqList;
            preFreqList = newFreqList;
        }
        preFreqList->insertNode(node);
    }

    void insertFreqNode(FreqNode* freqNode)    // 尾插,在外层链表中插入一个新的频度
    {
        freqNode->pre = dummyEnd->pre;
        freqNode->next = dummyEnd;
        dummyEnd->pre->next = freqNode;
        dummyEnd->pre = freqNode;
    }
public:
    LFUCache(int capacity) {
        this->capacity = capacity;
        dummyHead = new FreqNode(-1);
        dummyEnd = new FreqNode(-1);
        dummyHead->next = dummyEnd;
        dummyEnd->pre = dummyHead;
    }

    int get(int key) {
        // 如果键不存在于缓存中,返回 -1
        if(map.find(key) == map.end())
            return -1;
        // 如果键存在于缓存中,返回键的值
        else
        {
            Node* node = map[key];
            freqInc(node);
            return node->val;
        }
    }

    void put(int key, int value) {
        if(capacity == 0)    // 注意特殊情况,容量可能为 0,则不操作
            return;

        // 如果键不存在
        if(map.find(key) == map.end())
        {
            // 如果缓存容量已满,先删除最近最久未使用的节点(频度最小的(外层最右段)频度链表中的尾节点)
            if(map.size() == capacity)
            {
                FreqNode* minFreqList = dummyEnd->pre;
                Node* deleteNode = minFreqList->afterEnd->pre;
                map.erase(deleteNode->key);
                minFreqList->deleteNode(deleteNode);
                if(minFreqList->isEmpty())
                    deleteFreqNode(minFreqList);
            }
            // 插入新节点
            Node* newNode = new Node(key, value);
            map[key] = newNode;
            FreqNode* freqList = dummyEnd->pre;
            if(freqList->freq != 1)
            {
                FreqNode* newFreqList = new FreqNode(1);
                insertFreqNode(newFreqList);
                freqList = newFreqList;
            }
            freqList->insertNode(newNode);
        }
        // 如果键存在,更新键对应的值,频度+1
        else
        {
            Node* node = map[key];
            node->val = value;
            freqInc(node);
        }
    }
};

/**
 * 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);
 */

执行结果:

执行结果:通过

执行用时:380 ms, 在所有 C++ 提交中击败了 24.71% 的用户
内存消耗:173 MB, 在所有 C++ 提交中击败了 19.54% 的用户