【每日一题Day337】LC460LFU 缓存 | 双链表+哈希表

LFU 缓存【LC460】

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

实现 LFUCache 类:

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

为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。

当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 getput 操作,使用计数器的值将会递增。

函数 getput 必须以 O(1) 的平均时间复杂度运行。

  • 错误答案

    java 复制代码
    class LFUCache {
        // 注意:PriorityQueue要插入(删除)数据,顺序会发生改变,如果仅仅是修改已经稳定队列的值或内容,而不进行插入或者删除,那么,这个顺序是不会变的。
        // 使用Node存储每个key对应的value,以及该key的操作次数、最近操作时间
        // 小顶堆存储每个元素, 以使用次数及最近使用时间降序排序
        int capacity;
        PriorityQueue<Node> pq;
        Map<Integer, Node> map;
        int time;
        public LFUCache(int capacity) {
            this.capacity = capacity;
            this.pq = new PriorityQueue<>((o1, o2) -> o1.freq == o2.freq ? o1.time - o2.time : o1.freq - o2.freq);
            this.map = new HashMap<>();
            this.time = 0;
        }
        
        public int get(int key) {
            if (!map.containsKey(key)) return -1;
            Node node = map.get(key);
            node.freq += 1;
            node.time = time++;
            return node.val;
        }
        
        public void put(int key, int value) {
            if (map.containsKey(key)){
                Node node = map.get(key);
                node.val = value;
                node.freq += 1;
                node.time = time++;
            }else{
                Node node = new Node(key, value, time++, 1);
                if (pq.size() == capacity){
                    Node node1 = pq.poll();
                    map.remove(node1.key);
                }
                pq.add(node);
                map.put(key, node);
            }
        }
    }
    class Node{
        int key;
        int val;
        int time;
        int freq;
        public Node(){
            this.freq = 0;
        }
        public Node(int key, int val, int time, int freq){
            this.key = key;
            this.val = val;
            this.time = time;
            this.freq = freq;
        }
    }
    
    /**
     * 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);
     */
  • 思路

    • LFU和LRU不同之处
      • LRU移除元素时移除最近最久未使用的元素
      • LFU移除元素时移除使用频率最少最近最久未使用的元素
    • 因此LFU实现可以仿照LRU,使用哈希表记录每个频率下的节点,相同频率下的各个节点以双向链表的形式组织
      • 每次移除元素时移除频率最小的最久未使用的元素
      • 每操作一次元素需要修改节点频率至相应链表
      • 链表的首部存放最新操作的元素
      • 为了快速找到某个节点在链表中的位置,可以使用哈希表存储每个key对应的节点
      • 为了快速找到最小频率,维护一个int变量记录最小频率
  • 实现

    java 复制代码
    class LFUCache {
        private static class Node {
            int key, value, freq = 1; // 新书只读了一次
            Node prev, next;
    
            Node(int key, int value) {
                this.key = key;
                this.value = value;
            }
        }
    
        private final int capacity;
        private final Map<Integer, Node> keyToNode = new HashMap<>();
        private final Map<Integer, Node> freqToDummy = new HashMap<>();
        private int minFreq;
    
        public LFUCache(int capacity) {
            this.capacity = capacity;
        }
    
        public int get(int key) {
            Node node = getNode(key);
            return node != null ? node.value : -1;
        }
    
        public void put(int key, int value) {
            Node node = getNode(key);
            if (node != null) { // 有这本书
                node.value = value; // 更新 value
                return;
            }
            if (keyToNode.size() == capacity) { // 书太多了
                Node dummy = freqToDummy.get(minFreq);
                Node backNode = dummy.prev; // 最左边那摞书的最下面的书
                keyToNode.remove(backNode.key);
                remove(backNode); // 移除
                if (dummy.prev == dummy) { // 这摞书是空的
                    freqToDummy.remove(minFreq); // 移除空链表
                }
            }
            node = new Node(key, value); // 新书
            keyToNode.put(key, node);
            pushFront(1, node); // 放在「看过 1 次」的最上面
            minFreq = 1;
        }
    
        private Node getNode(int key) {
            if (!keyToNode.containsKey(key)) { // 没有这本书
                return null;
            }
            Node node = keyToNode.get(key); // 有这本书
            remove(node); // 把这本书抽出来
            Node dummy = freqToDummy.get(node.freq);
            if (dummy.prev == dummy) { // 抽出来后,这摞书是空的
                freqToDummy.remove(node.freq); // 移除空链表
                if (minFreq == node.freq) { // 这摞书是最左边的
                    minFreq++;
                }
            }
            pushFront(++node.freq, node); // 放在右边这摞书的最上面
            return node;
        }
    
        // 创建一个新的双向链表
        private Node newList() {
            Node dummy = new Node(0, 0); // 哨兵节点
            dummy.prev = dummy;
            dummy.next = dummy;
            return dummy;
        }
    
        // 在链表头添加一个节点(把一本书放在最上面)
        private void pushFront(int freq, Node x) {
            Node dummy = freqToDummy.computeIfAbsent(freq, k -> newList());
            x.prev = dummy;
            x.next = dummy.next;
            x.prev.next = x;
            x.next.prev = x;
        }
    
        // 删除一个节点(抽出一本书)
        private void remove(Node x) {
            x.prev.next = x.next;
            x.next.prev = x.prev;
        }
    }
相关推荐
海海不掉头发5 小时前
苍穹外卖-day05redis 缓存的学习
学习·缓存
川石教育7 小时前
Vue前端开发-缓存优化
前端·javascript·vue.js·缓存·前端框架·vue·数据缓存
DT辰白7 小时前
基于Redis的网关鉴权方案与性能优化
数据库·redis·缓存
岁月变迁呀1 天前
Redis梳理
数据库·redis·缓存
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭1 天前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
Code apprenticeship1 天前
怎么利用Redis实现延时队列?
数据库·redis·缓存
fpcc1 天前
跟我学c++中级篇——C++中的缓存利用
c++·缓存
Ewen Seong1 天前
mysql系列5—Innodb的缓存
数据库·mysql·缓存
安全二次方security²1 天前
SMMU软件指南SMMU编程之虚拟机结构和缓存
缓存·cache·smmu·arm安全架构·系统mmu·虚拟机结构·vms
.生产的驴1 天前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven