题目链接

LeetCode

题目描述

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

实现 LFUCache 类:

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

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

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

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

示例:

输入:
[“LFUCache”, “put”, “put”, “get”, “put”, “get”, “get”, “put”, “get”, “get”, “get”]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]]
输出:
[null, null, null, 1, null, -1, 3, null, -1, 3, 4]

解释:
// cnt(x) = 键 x 的使用计数
// cache=[] 将显示最后一次使用的顺序(最左边的元素是最近的)
LFUCache lFUCache = new LFUCache(2);
lFUCache.put(1, 1); // cache=[1,_], cnt(1)=1
lFUCache.put(2, 2); // cache=[2,1], cnt(2)=1, cnt(1)=1
lFUCache.get(1); // 返回 1
// cache=[1,2], cnt(2)=1, cnt(1)=2
lFUCache.put(3, 3); // 去除键 2 ,因为 cnt(2)=1 ,使用计数最小
// cache=[3,1], cnt(3)=1, cnt(1)=2
lFUCache.get(2); // 返回 -1(未找到)
lFUCache.get(3); // 返回 3
// cache=[3,1], cnt(3)=2, cnt(1)=2
lFUCache.put(4, 4); // 去除键 1 ,1 和 3 的 cnt 相同,但 1 最久未使用
// cache=[4,3], cnt(4)=1, cnt(3)=2
lFUCache.get(1); // 返回 -1(未找到)
lFUCache.get(3); // 返回 3
// cache=[3,4], cnt(4)=1, cnt(3)=3
lFUCache.get(4); // 返回 4
// cache=[3,4], cnt(4)=2, cnt(3)=3

提示:

  • 0 <= capacity, key, value <= 104
  • 最多调用 105getput 方法

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

解题思路

方法一:哈希表 + 链表(超时)

节点中有 key value 和使用频率
通过哈希表记录当前存在的节点

对于新加入的节点和新使用过的节点,移动到链表最前面(越靠前越近被使用过LRU)并使用频率加一
插入节点

  1. 判断当前节点key是否存在,如果存在 则 更新 value 使用频率加一 并将节点移动到链表首部
  2. 如果存在则判断当前 size 是否达到容量,如果没达到 添加节点并移动到链表首部 size++
  3. 如果达到容量,将使用频率最小的节点删除 添加节点。如果频率最小的节点不唯一,则删除链表最后一个使用频率最小的节点 ```java

class LFUCache { class CacheNode{ private int key; private int value; private int frequency; CacheNode next; CacheNode pre; CacheNode(){ this.key = 0; this.value = 0; this.frequency = 0; next = null; pre = null; } CacheNode(int key, int value){ this.key = key; this.value = value; this.frequency = 0; next = null; pre = null; } } private int capacity; private Map cache; private int size; private CacheNode head, tail; public LFUCache(int capacity) { this.capacity = capacity; cache = new HashMap(); this.size = 0; head = new CacheNode(); tail = new CacheNode(); head.next = tail; tail.pre = head; }

  1. public int get(int key) {
  2. if(capacity == 0){
  3. return -1;
  4. }
  5. if(cache.containsKey(key)){
  6. CacheNode node = cache.get(key);
  7. ++node.frequency;
  8. moveToHead(node);
  9. return node.value;
  10. }
  11. return -1;
  12. }
  13. public void put(int key, int value) {
  14. // 存在 key 替换结果
  15. if(capacity == 0){
  16. return;
  17. }
  18. if(cache.containsKey(key)){
  19. CacheNode node = cache.get(key);
  20. ++node.frequency;
  21. node.value = value;
  22. moveToHead(node);
  23. }else{
  24. // 创建节点
  25. CacheNode node = new CacheNode(key, value);
  26. // 容量满了 删除一个
  27. if(size == capacity){
  28. CacheNode cur = head.next;
  29. int minVal = head.next.frequency;
  30. CacheNode minNode = head.next;
  31. int num = 0;
  32. while(cur != tail){
  33. // 记录使用频率最少的节点
  34. if(cur.frequency < minVal){
  35. minVal = cur.frequency;
  36. minNode = cur;
  37. num = 1;
  38. }else if(cur.frequency == minVal){
  39. // 频率相同时 选最长时间没被使用的节点
  40. minNode = cur;
  41. ++num;
  42. }
  43. cur = cur.next;
  44. }
  45. // 删除节点
  46. cache.remove(minNode.key);
  47. deleteNode(minNode);
  48. // 添加节点
  49. cache.put(key, node);
  50. addToHead(node);
  51. }else{
  52. ++size;
  53. cache.put(key, node);
  54. addToHead(node);
  55. }
  56. }
  57. }
  58. private void moveToHead(CacheNode node){
  59. deleteNode(node);
  60. addToHead(node);
  61. }
  62. private void addToHead(CacheNode node){
  63. node.next = head.next;
  64. node.pre = head;
  65. head.next.pre = node;
  66. head.next = node;
  67. }
  68. private void deleteNode(CacheNode node){
  69. CacheNode preNode = node.pre;
  70. preNode.next = node.next;
  71. node.next.pre = preNode;
  72. node.next = null;
  73. node.pre = null;
  74. }

}

/**

  • 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); */ ```
  • 时间复杂度 O(n)
  • 空间复杂度 O(n)

    方法二:哈希表 + 小顶堆(Lambda表达式或者compareTo)

    利用重写compareTo 对优先队列进行排序,堆顶永远是 使用频率最小 或者 频率相同 使用顺序最小(最早使用) 的节点
    通过 idx 表示访问的先后 ```java class LFUCache {

    Map cache; Queue queue; int capacity; int size; // idx 表示访问的先后 int idx = 0;

    public LFUCache(int capacity) {

      cache = new HashMap<>(capacity);
      if (capacity > 0) {
          queue = new PriorityQueue<>(capacity);
          // 排序推荐用 lambda表达式实现
          // queue = new PriorityQueue<Node>((a, b)-> a.freq - b.freq == 0 ? a.idx - b.idx:a.freq - b.freq);
      }
      this.capacity = capacity;
    

    }

    public int get(int key) {

      // 不存在
      if (!cache.containsKey(key)) {
          return -1;
      }
      Node node = cache.get(key);
      node.freq++;
      node.idx = idx++;
      // 删除node 再添加 为了重新排序   
      queue.remove(node);
      queue.offer(node);
      return node.value;
    

    }

    public void put(int key, int value) {

      if (capacity == 0) {
          return;
      }
      Node node = cache.get(key);
      if (node != null) {
          node.value = value;
          node.freq++;
          node.idx = idx++;
          // 重新排序
          queue.remove(node);
          queue.offer(node);
      } else {
          if (size == capacity) {
              // 删除 在堆顶的节点
              cache.remove(queue.peek().key);
              queue.poll();
              size--;
          }
          // 添加新的节点 
          Node newNode = new Node(key, value, idx++);
          cache.put(key, newNode);
          queue.offer(newNode);
          size++;
      }
    

    } }

class Node implements Comparable { int key; int value; int freq; int idx;

public Node() {}

public Node(int key, int value, int idx) {
    this.key = key;
    this.value = value;
    freq = 1;
    this.idx = idx;
}
// 重写比较函数  频率不同时按频率排序  频率相同时按先后顺序排序
public int compareTo(Node node) {
    int diff = freq - node.freq;
    return diff != 0? diff: idx - node.idx;
}

}

```java
class LFUCache {
    Map<Integer, Node> cache;
    Queue<Node> queue;
    int capacity;
    int size;
    // idx 表示访问的先后
    int idx = 0;
    public LFUCache(int capacity) {
        this.capacity = capacity;
        cache = new HashMap<Integer, Node>();
        // 重写比较函数  频率不同时按频率排序  频率相同时按先后顺序排序
        queue = new  PriorityQueue<Node>((a, b) -> {
            return a.freq != b.freq ? a.freq - b .freq : a.idx - b.idx;
        });
    }

    public int get(int key) {
        // 不存在
        if (!cache.containsKey(key)) {
            return -1;
        }
        Node node = cache.get(key);
        node.freq++;
        node.idx = idx++;
        queue.remove(node);
        queue.offer(node);
        return node.value;
    }

    public void put(int key, int value) {
        if(capacity == 0){
            return;
        }
        Node node = cache.get(key);
        if(node != null){
            node.freq++;
            node.idx = idx++;
            node.value = value;
            queue.remove(node);
            queue.offer(node);
        }else{
            if(capacity <= cache.size()){
                cache.remove(queue.peek().key);
                queue.poll();
            }
            node = new Node(key, value, idx++);
            cache.put(key, node);
            queue.offer(node);
        }
    }

    class Node {
        int key;
        int value;
        int freq;
        int idx;

        public Node() {}

        public Node(int key, int value, int idx) {
            this.key = key;
            this.value = value;
            freq = 1;
            this.idx = idx;
        }
    }
}

/**
 * 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);
 */
  • 时间复杂度 O(n log n)
  • 空间复杂度 O(capacity)

    方法三:双哈希表

    利用重写compareTo 对优先队列进行排序,堆顶永远是 使用频率最小 或者 频率相同 使用顺序最小(最早使用) 的节点
    通过 idx 表示访问的先后 ```java class LFUCache { int minfreq, capacity; Map key_table; Map> freq_table;

    public LFUCache(int capacity) {

      this.minfreq = 0;
      this.capacity = capacity;
      key_table = new HashMap<Integer, Node>();;
      freq_table = new HashMap<Integer, LinkedList<Node>>();
    

    }

    public int get(int key) {

      if (capacity == 0) {
          return -1;
      }
      if (!key_table.containsKey(key)) {
          return -1;
      }
      Node node = key_table.get(key);
      int val = node.val, freq = node.freq;
      freq_table.get(freq).remove(node);
      // 如果当前链表为空,我们需要在哈希表中删除,且更新minFreq
      if (freq_table.get(freq).size() == 0) {
          freq_table.remove(freq);
          if (minfreq == freq) {
              minfreq += 1;
          }
      }
      // 插入到 freq + 1 中
      LinkedList<Node> list = freq_table.getOrDefault(freq + 1, new LinkedList<Node>());
      list.offerFirst(new Node(key, val, freq + 1));
      freq_table.put(freq + 1, list);
      key_table.put(key, freq_table.get(freq + 1).peekFirst());
      return val;
    

    }

    public void put(int key, int value) {

      if (capacity == 0) {
          return;
      }
      if (!key_table.containsKey(key)) {
          // 缓存已满,需要进行删除操作
          if (key_table.size() == capacity) {
              // 通过 minFreq 拿到 freq_table[minFreq] 链表的末尾节点
              Node node = freq_table.get(minfreq).peekLast();
              key_table.remove(node.key);
              freq_table.get(minfreq).pollLast();
              if (freq_table.get(minfreq).size() == 0) {
                  freq_table.remove(minfreq);
              }
          }
          LinkedList<Node> list = freq_table.getOrDefault(1, new LinkedList<Node>());
          list.offerFirst(new Node(key, value, 1));
          freq_table.put(1, list);
          key_table.put(key, freq_table.get(1).peekFirst());
          minfreq = 1;
      } else {
          // 与 get 操作基本一致,除了需要更新缓存的值
          Node node = key_table.get(key);
          int freq = node.freq;
          freq_table.get(freq).remove(node);
          if (freq_table.get(freq).size() == 0) {
              freq_table.remove(freq);
              if (minfreq == freq) {
                  minfreq += 1;
              }
          }
          LinkedList<Node> list = freq_table.getOrDefault(freq + 1, new LinkedList<Node>());
          list.offerFirst(new Node(key, value, freq + 1));
          freq_table.put(freq + 1, list);
          key_table.put(key, freq_table.get(freq + 1).peekFirst());
      }
    

    } }

class Node { int key, val, freq;

Node(int key, int val, int freq) {
    this.key = key;
    this.val = val;
    this.freq = freq;
}

} ```

  • 时间复杂度 O(1)
  • 空间复杂度 O(capacity)