🚩传送门:牛客题目

题目

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

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

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

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

🚩 注意:函数 getput 必须以 🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图1 的平均时间复杂度运行。

解题思路:双向链表 O( n)

最傻fufu の 🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图2 解法

链表排序准则优先按照 freq 排序,当 freq 相等时,按照时间戳排序,时间久的排前面,创建时间端,因此满了之后删除 head.post,该 Node 即 freq 最小且最久访问的。

每次 node freq++ 后,从当前位置向后遍历链表,直到 nextNode.freq > node.freq || nextNode == tail,在 nextNode 之前插入该 node
image.png

复杂度分析

时间复杂度:🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图4,其中 🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图5 指的是 capacity 大小。

空间复杂度:🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图6,其中 🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图7 指的是 capacity 大小。

官方代码

  1. // 双向链表结点类
  2. class Node {
  3. int key;
  4. int value;
  5. int freq = 1;
  6. Node pre;
  7. Node post;
  8. public Node() {}
  9. public Node(int key, int value) {
  10. this.key = key;
  11. this.value = value;
  12. }
  13. }
  14. // LFU 缓存类
  15. class LFUCache {
  16. // Map 缓存 <key,Node>
  17. HashMap<Integer, Node> cache;
  18. Node head;
  19. Node tail;
  20. int capacity;
  21. int size;
  22. // 初始化
  23. public LFUCache(int capacity) {
  24. cache = new HashMap<Integer, Node>(capacity);
  25. this.capacity = capacity;
  26. head = new Node();
  27. tail = new Node();
  28. head.post = tail;
  29. tail.pre = head;
  30. }
  31. // 获取
  32. public int get(int key) {
  33. Node node = cache.get(key);
  34. if (node == null) {
  35. return -1;
  36. }
  37. node.freq++;
  38. moveToNewPosition(node);
  39. return node.value;
  40. }
  41. public void put(int key, int value) {
  42. if (capacity == 0) {
  43. return;
  44. }
  45. Node node = cache.get(key);
  46. // 插入 <key,value> 在缓存中,则热度增加,重新排序
  47. if (node != null) {
  48. node.value = value;
  49. node.freq++;
  50. moveToNewPosition(node);
  51. } else {
  52. // 插入 <key,value> 不在缓存中,查看容量是否满了
  53. if (size == capacity) {
  54. // 满了移除热度最小最久的 node
  55. cache.remove(head.post.key);
  56. removeNode(head.post);
  57. size--;
  58. }
  59. // 重新添加新的 node
  60. Node newNode = new Node(key, value);
  61. addNode(newNode);
  62. cache.put(key, newNode);
  63. size++;
  64. }
  65. }
  66. private void moveToNewPosition(Node node) {
  67. Node nextNode = node.post;
  68. removeNode(node);
  69. while (nextNode.freq <= node.freq && nextNode != tail) {
  70. nextNode = nextNode.post;
  71. }
  72. nextNode.pre.post = node;
  73. node.pre = nextNode.pre;
  74. node.post = nextNode;
  75. nextNode.pre = node;
  76. }
  77. private void addNode(Node node) {
  78. node.post = head.post;
  79. node.pre = head;
  80. head.post.pre = node;
  81. head.post = node;
  82. moveToNewPosition(node);
  83. }
  84. private void removeNode(Node node) {
  85. node.pre.post = node.post;
  86. node.post.pre = node.pre;
  87. }
  88. }

解题思路:优先队列、最小堆 O( logN)

将访问频次 freq 最小的且最先访问的上浮到堆顶,下面用 idx = System.currentTimeMillis() 比较访问时间的先后。

若全局自增 idx 表示访问的先后,一旦溢出就 BUG

复杂度分析

时间复杂度:🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图8,其中 🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图9 指的是 capacity 大小。

空间复杂度:🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图10,其中 🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图11 指的是 capacity 大小。

官方代码

  1. class Node {
  2. int key, val, freq;
  3. Node(int key, int val, int freq) {
  4. this.key = key;
  5. this.val = val;
  6. this.freq = freq;
  7. }
  8. }
  9. class LFUCache {
  10. Map<Integer, Node> cache; // Map 做缓存用于 O(logN) Put 和 Get
  11. Queue<Node> queue; // 优先队列做排序结构
  12. int capacity;
  13. int size;
  14. int idx = 0;
  15. public LFUCache(int capacity) {
  16. cache = new HashMap<>(capacity);
  17. if (capacity > 0) {
  18. queue = new PriorityQueue<>(capacity); // 定义 capacity 容量的最小堆
  19. }
  20. this.capacity = capacity;
  21. }
  22. public int get(int key) {
  23. Node node = cache.get(key);
  24. if (node == null) {
  25. return -1;
  26. }
  27. node.freq++;
  28. node.idx = System.currentTimeMillis();
  29. queue.remove(node);
  30. queue.offer(node);
  31. return node.value;
  32. }
  33. public void put(int key, int value) {
  34. if (capacity == 0) {
  35. return;
  36. }
  37. Node node = cache.get(key);
  38. if (node != null) {
  39. // 当前 <key,value> 已经被缓存
  40. node.value = value;
  41. node.freq++;
  42. node.idx = System.currentTimeMillis();
  43. queue.remove(node);
  44. queue.offer(node);
  45. } else {
  46. // 当前 <key,value> 未被缓存,且满容量
  47. if (size == capacity) {
  48. cache.remove(queue.peek().key); // 缓存中移除最小最久热度
  49. queue.poll(); // 结构中移除最小最久热度
  50. size--;
  51. }
  52. Node newNode = new Node(key, value, System.currentTimeMillis());
  53. cache.put(key, newNode); // 缓存中添加最新热度结点
  54. queue.offer(newNode); // 结构中添加最新热度结点
  55. size++;
  56. }
  57. }
  58. }
  59. class Node implements Comparable<Node> {
  60. int key;
  61. int value;
  62. int freq;
  63. int idx;
  64. public Node() {}
  65. public Node(int key, int value, int idx) {
  66. this.key = key;
  67. this.value = value;
  68. freq = 1;
  69. this.idx = idx;
  70. }
  71. // 比较准则:优先比较频率,频率相同比较时间戳
  72. public int compareTo(Node node) {
  73. int diff = freq - node.freq;
  74. return diff != 0? diff: idx - node.idx;
  75. }
  76. }

解题思路:双HashMap、自写双向链表 Remove(地址删除) O(1)

image.png

对于具有相同的 freq Node ,新 Node 在双向链表中使用头插法,因此链表的尾部则是最久不使用的。
由于 LinkedListremove 删除是 🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图13 ,因此想让 get、put🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图14 内,需要自己写 remove(地址) 。

注意:用官方的 LinkedList 实现的双向链表不是 🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图15 而是 🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图16 ,其 remove 函数是顺序查找,非按地址删除

复杂度分析

时间复杂度:🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图17,所有的操作均在 🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图18 内完成 。

空间复杂度:🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图19,其中 🚩[NC]94. 设计LFU缓存结构 【双向链表 双 HashMap】 - 图20 指的是 capacity 大小。

官方代码

  1. class Node {
  2. int key;
  3. int value;
  4. int freq ; // 保存频率
  5. Node pre; // Node所在频次的双向链表的前继Node
  6. Node post; // Node所在频次的双向链表的后继Node
  7. public Node() {
  8. freq = 1; // 无参构造函数默认创建频率 1
  9. }
  10. public Node(int key, int value,int freq) {
  11. this.key = key;
  12. this.value = value;
  13. this.freq = freq;
  14. }
  15. }
  16. class DoublyLinkedList {
  17. Node head; // 该双向链表的头节点,新节点从头部加入,表示最近访问
  18. Node tail; // 该双向链表的尾节点,删除节点从尾部删除,表示最久访问
  19. int capacity;
  20. public DoublyLinkedList() {
  21. capacity=0; // 初始化容量为 0
  22. head = new Node();
  23. tail = new Node();
  24. head.post = tail;
  25. tail.pre = head;
  26. }
  27. int size(){
  28. return capacity;
  29. }
  30. Node peekFirst() { // 返回首结点不删除
  31. if (head.post != tail)
  32. return head.post;
  33. return null;
  34. }
  35. Node pollFirst() { // 返回首结点删除
  36. Node node=null;
  37. if (head.post != tail){
  38. node = new Node(head.post.key,head.post.value,head.post.freq);
  39. remove(head.post);
  40. }
  41. return node;
  42. }
  43. Node peekLast() {
  44. if (tail.pre != head)
  45. return tail.pre;
  46. return null;
  47. }
  48. Node pollLast() {
  49. Node node=null;
  50. if (tail.pre != head){
  51. node = new Node(tail.pre.key,tail.pre.value,tail.pre.freq);
  52. remove(tail.pre);
  53. }
  54. return node;
  55. }
  56. void remove(Node node) {
  57. if(node==null||capacity==0) return;
  58. node.pre.post = node.post;
  59. node.post.pre = node.pre;
  60. capacity--;
  61. }
  62. // 头插法
  63. void addNode(Node node) {
  64. node.post = head.post;
  65. head.post.pre = node;
  66. head.post = node;
  67. node.pre = head;
  68. capacity++;
  69. }
  70. }
  71. class LFUCache {
  72. int minfreq, capacity;
  73. Map<Integer, Node> key_table;
  74. Map<Integer, DoublyLinkedList> freq_table;
  75. public LFUCache(int capacity) {
  76. this.minfreq = 0;
  77. this.capacity = capacity;
  78. key_table = new HashMap<Integer, Node>(); // 维护O(1)查找、插入
  79. freq_table = new HashMap<Integer, DoublyLinkedList>(); // 维护删除过时结构
  80. }
  81. public int get(int key) {
  82. if (capacity == 0) {
  83. return -1;
  84. }
  85. if (!key_table.containsKey(key)) {
  86. return -1;
  87. }
  88. Node node = key_table.get(key);
  89. int val = node.value, freq = node.freq;
  90. freq_table.get(freq).remove(node); // 在O(1)内删除结点
  91. // 如果当前链表为空,我们需要在哈希表中删除,且更新minFreq
  92. if (freq_table.get(freq).size() == 0) {
  93. freq_table.remove(freq);
  94. if (minfreq == freq) {
  95. minfreq++;
  96. }
  97. }
  98. // 插入到 freq + 1 中
  99. DoublyLinkedList list = freq_table.getOrDefault(freq + 1, new DoublyLinkedList());
  100. list.addNode(new Node(key, val, freq + 1));
  101. freq_table.put(freq + 1, list);
  102. key_table.put(key, freq_table.get(freq + 1).peekFirst());
  103. return val;
  104. }
  105. public void put(int key, int value) {
  106. if (capacity == 0) {
  107. return;
  108. }
  109. if (!key_table.containsKey(key)) {
  110. // 缓存已满,需要进行删除操作
  111. if (key_table.size() == capacity) {
  112. // 通过 minFreq 拿到 freq_table[minFreq] 链表的末尾节点
  113. Node node = freq_table.get(minfreq).peekLast();
  114. key_table.remove(node.key);
  115. freq_table.get(minfreq).pollLast();
  116. if (freq_table.get(minfreq).size() == 0) {
  117. freq_table.remove(minfreq);
  118. }
  119. }
  120. DoublyLinkedList list = freq_table.getOrDefault(1, new DoublyLinkedList());
  121. list.addNode(new Node(key, value, 1));
  122. freq_table.put(1, list);
  123. key_table.put(key, freq_table.get(1).peekFirst());
  124. minfreq = 1;
  125. } else {
  126. // 与 get 操作基本一致,除了需要更新缓存的值
  127. Node node = key_table.get(key);
  128. int freq = node.freq;
  129. freq_table.get(freq).remove(node);
  130. if (freq_table.get(freq).size() == 0) {
  131. freq_table.remove(freq);
  132. if (minfreq == freq) {
  133. minfreq++;
  134. }
  135. }
  136. DoublyLinkedList list = freq_table.getOrDefault(freq + 1, new DoublyLinkedList());
  137. list.addNode(new Node(key, value, freq + 1));
  138. freq_table.put(freq + 1, list);
  139. key_table.put(key, freq_table.get(freq + 1).peekFirst());
  140. }
  141. }
  142. }