一文详解LFU Cache算法

237 阅读4分钟

题目描述

实现 LFUCache 类:

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

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

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

罗列条件

  1. get(key),若key存在,则返回value,该key的计数器加1

  2. get(key),若key不存在,则返回-1

  3. put(key, value),若key存在,更新value,该key的计数器加1

  4. put(key, value),若key不存在

    1. 若容量 capacity已满,将计数器最小删除,若存在多个,则最近最久未使用的值删除,将(key,value)存入容器,该key的计数器等于1
    2. 若容器 capacity未满,将(key,value)存入容器,该key的计数器等于1
  5. get 和 put平均时间复杂度 必须是 O(1)

思考实现

  1. get 和 put时间复杂度为 O(1) ,最有可能用到HashMap
  2. key,value,计数器是几个关联性强的属性,可以单独定义在一个数据结构Node中,那么可以定义HashMap<key, Node>容器,完成get时间复杂度为 O(1)的要求
  3. put中存在删除,考虑时间复杂度,删除操作O(1),最有可能用到(双向)链表DoubleLinkedNode
  4. 删除要求计数器最小,如何知道计数器最小是多少,简单处理,定义个全局变量minFreq记录计数器最小
  5. 如何 O(1)时间内根据minFreq获取到它的Node,可以定义HashMap<Freq, Node>,考虑相同频率可能有多个,且有删除操作,所以变动下,定义HashMap<Freq, DoubleLinkedNode>结构
  6. 删除计数器最小时,若存在多个,则删除DoubleLinkedNode中最近最久未使用的值,如何 O(1)时间内删除最近最久未使用呢,只要 O(1)时间内查找到最近最久未使用,那就可以实现,因为链表删除操作时间复杂度就是O(1)
  7. 如何O(1)时间内查找到DoubleLinkedNode中最近最久未使用,最简单的做法,将DoubleLinkedNode的头结点或者尾结点专门存最近最久未使用的Node,那查找起来时间为O(1),一般是尾结点,为什么这么设置,因为每次新访问的值,在头部插入,越靠前,越是最近访问,越靠后越是最久(未使用)访问,最久未使用的必然是尾结点
  8. 考虑get的实现流程,不存在的流程不做说明,存在key时,key对应的node的计数器值要加1,计数器加1后,它就需要从HashMap<Freq, DoubleLinkedNode>结构挪位置,不能再原来的计数器链表中存储了,需要从里面删除掉,然后插入加1后的计数器链表中,这里容易遗漏一个细节,若原来计数器所在链表一个node都没有,需要从HashMap<Freq, DoubleLinkedNode>删掉。此时还需要判断是否minFreq是否是这个计数器值,若是则要加1
  9. 考虑put的实现流程,若key存在,流程与get的流程基本一致,多了一步更新value;若key不存在,则需要插入新值,首先需要判断当前容量是否满,满的话,需要先删除一个值,即计数为minFreq值,通过minFreq拿到DoubleLinkedNode,然后删除尾结点(前面有解释尾节点即为最久未使用的结点),从HashMap<key, Node>容器中删掉。同样的,若原来计数器所在链表一个node都没有,需要从HashMap<Freq, DoubleLinkedNode>删掉。依据传入的key,value构建新的node,放入计数器为1的HashMap<Freq, DoubleLinkedNode>和HashMap<key, Node>容器中,minFreq赋值为1

编码

public class LFUCache {
    class Node {
        private int key;
        private int value;
        private int frequency;
        private Node pre;
        private Node next;

        public Node() {
            this(-1, -1, 0);
        }

        public Node(int key, int value, int frequency) {
            this.key = key;
            this.value = value;
            this.frequency = frequency;
        }
    }
    // 带有头结点和尾节点的双向链表,方便头部插入和尾部获取
     class DoubleLinkedNode {
        private Node head;
        private Node tail;
        private int size;

        public DoubleLinkedNode() {
            head = new Node();
            tail = new Node();
            head.next = tail;
            tail.pre = head;
            size =0;
        }

        public void insertHead(Node node) {
            node.pre = head;
            node.next = head.next;
            head.next.pre = node;
            head.next = node;
            size++;
        }

        public void remove(Node node) {
            node.pre.next = node.next;
            node.next.pre = node.pre;
            size--;
        }

        public Node getHead() {
            return head.next;
        }

        public Node getTail() {
            return tail.pre;
        }
    }
    
    private HashMap<Integer, Node> keyMap = new HashMap<>();
    private HashMap<Integer, DoubleLinkedNode> freqMap = new HashMap<>();

    private int capacity;
    private int minFreq;
    
    public LFUCache(int capacity) {
        this.capacity = capacity;
        this.minFreq = 0;
    }
    
    public int get(int key) {
        Node node = keyMap.get(key);
        if (node == null) {
            return -1;
        }

        int freq = node.frequency;
        DoubleLinkedNode oldFreq = freqMap.get(freq);
        oldFreq.remove(node);
        if (oldFreq.size == 0) {
            freqMap.remove(freq);
            if (minFreq == freq) {
                minFreq++;
            }
        }

        node.frequency = freq + 1;
        DoubleLinkedNode newFreq = freqMap.getOrDefault(freq + 1, new DoubleLinkedNode());
        newFreq.insertHead(node);
        freqMap.put(freq+1, newFreq);
        return node.value;
    }
    
    public void put(int key, int value) {
        if (0 >= capacity) {
            return;
        }
        
        Node node = keyMap.get(key);
        if (null == node) {
           
            if (capacity == keyMap.size()) {
                DoubleLinkedNode minFreqList = freqMap.get(minFreq);
                Node tail = minFreqList.getTail();
                minFreqList.remove(tail);
                if (minFreqList.size == 0) {
                    freqMap.remove(minFreq);
                }
                keyMap.remove(tail.key);
            }

            node = new Node(key, value, 1);
            keyMap.put(key, node);
            DoubleLinkedNode newFreq = freqMap.getOrDefault(1, new DoubleLinkedNode());
            newFreq.insertHead(node);
            freqMap.put(1, newFreq);
            minFreq = 1;
        } else {
            node.value = value;
            int freq = node.frequency;
            DoubleLinkedNode oldFreq = freqMap.get(freq);
            oldFreq.remove(node);
            if (oldFreq.size == 0) {
                freqMap.remove(freq);
                if (minFreq == freq) {
                    minFreq++;
                }
            }

            node.frequency = freq + 1;
            DoubleLinkedNode newFreq = freqMap.getOrDefault(freq + 1, new DoubleLinkedNode());
            newFreq.insertHead(node);
            freqMap.put(freq+1, newFreq);
        }
    }
}

测试

        LFUCache lfu = new LFUCache(2);
        lfu.put(1,1);
        lfu.put(2,2);
        System.out.println(lfu.get(1)); // 1
        lfu.put(3,3);
        System.out.println(lfu.get(2)); // -1
        System.out.println(lfu.get(3));// 3
        lfu.put(4,4);
        System.out.println(lfu.get(1));// -1
        System.out.println(lfu.get(3));// 3
        System.out.println(lfu.get(4));// 4