🚩传送门:牛客题目
题目
请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。实现 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( n)
最傻fufu の 解法
链表排序准则优先按照 freq 排序,当 freq 相等时,按照时间戳排序,时间久的排前面,创建时间端,因此满了之后删除 head.post,该 Node 即 freq 最小且最久访问的。
每次 node 的 freq++ 后,从当前位置向后遍历链表,直到 nextNode.freq > node.freq || nextNode == tail,在 nextNode 之前插入该 node。
复杂度分析
时间复杂度:,其中
指的是 capacity 大小。
空间复杂度:,其中
指的是 capacity 大小。
官方代码
// 双向链表结点类class Node {int key;int value;int freq = 1;Node pre;Node post;public Node() {}public Node(int key, int value) {this.key = key;this.value = value;}}// LFU 缓存类class LFUCache {// Map 缓存 <key,Node>HashMap<Integer, Node> cache;Node head;Node tail;int capacity;int size;// 初始化public LFUCache(int capacity) {cache = new HashMap<Integer, Node>(capacity);this.capacity = capacity;head = new Node();tail = new Node();head.post = tail;tail.pre = head;}// 获取public int get(int key) {Node node = cache.get(key);if (node == null) {return -1;}node.freq++;moveToNewPosition(node);return node.value;}public void put(int key, int value) {if (capacity == 0) {return;}Node node = cache.get(key);// 插入 <key,value> 在缓存中,则热度增加,重新排序if (node != null) {node.value = value;node.freq++;moveToNewPosition(node);} else {// 插入 <key,value> 不在缓存中,查看容量是否满了if (size == capacity) {// 满了移除热度最小最久的 nodecache.remove(head.post.key);removeNode(head.post);size--;}// 重新添加新的 nodeNode newNode = new Node(key, value);addNode(newNode);cache.put(key, newNode);size++;}}private void moveToNewPosition(Node node) {Node nextNode = node.post;removeNode(node);while (nextNode.freq <= node.freq && nextNode != tail) {nextNode = nextNode.post;}nextNode.pre.post = node;node.pre = nextNode.pre;node.post = nextNode;nextNode.pre = node;}private void addNode(Node node) {node.post = head.post;node.pre = head;head.post.pre = node;head.post = node;moveToNewPosition(node);}private void removeNode(Node node) {node.pre.post = node.post;node.post.pre = node.pre;}}
解题思路:优先队列、最小堆 O( logN)
将访问频次 freq 最小的且最先访问的上浮到堆顶,下面用 idx = System.currentTimeMillis() 比较访问时间的先后。
若全局自增 idx 表示访问的先后,一旦溢出就 BUG 了
复杂度分析
时间复杂度:,其中
指的是 capacity 大小。
空间复杂度:,其中
指的是 capacity 大小。
官方代码
class Node {int key, val, freq;Node(int key, int val, int freq) {this.key = key;this.val = val;this.freq = freq;}}class LFUCache {Map<Integer, Node> cache; // Map 做缓存用于 O(logN) Put 和 GetQueue<Node> queue; // 优先队列做排序结构int capacity;int size;int idx = 0;public LFUCache(int capacity) {cache = new HashMap<>(capacity);if (capacity > 0) {queue = new PriorityQueue<>(capacity); // 定义 capacity 容量的最小堆}this.capacity = capacity;}public int get(int key) {Node node = cache.get(key);if (node == null) {return -1;}node.freq++;node.idx = System.currentTimeMillis();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) {// 当前 <key,value> 已经被缓存node.value = value;node.freq++;node.idx = System.currentTimeMillis();queue.remove(node);queue.offer(node);} else {// 当前 <key,value> 未被缓存,且满容量if (size == capacity) {cache.remove(queue.peek().key); // 缓存中移除最小最久热度queue.poll(); // 结构中移除最小最久热度size--;}Node newNode = new Node(key, value, System.currentTimeMillis());cache.put(key, newNode); // 缓存中添加最新热度结点queue.offer(newNode); // 结构中添加最新热度结点size++;}}}class Node implements Comparable<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;}// 比较准则:优先比较频率,频率相同比较时间戳public int compareTo(Node node) {int diff = freq - node.freq;return diff != 0? diff: idx - node.idx;}}
解题思路:双HashMap、自写双向链表 Remove(地址删除) O(1)

对于具有相同的 freq 的 Node ,新 Node 在双向链表中使用头插法,因此链表的尾部则是最久不使用的。
由于 LinkedList 的 remove 删除是 ,因此想让
get、put 在 内,需要自己写 remove(地址) 。
注意:用官方的 LinkedList 实现的双向链表不是 而是
,其 remove 函数是顺序查找,非按地址删除
复杂度分析
时间复杂度:,所有的操作均在
内完成 。
空间复杂度:,其中
指的是 capacity 大小。
官方代码
class Node {int key;int value;int freq ; // 保存频率Node pre; // Node所在频次的双向链表的前继NodeNode post; // Node所在频次的双向链表的后继Nodepublic Node() {freq = 1; // 无参构造函数默认创建频率 1}public Node(int key, int value,int freq) {this.key = key;this.value = value;this.freq = freq;}}class DoublyLinkedList {Node head; // 该双向链表的头节点,新节点从头部加入,表示最近访问Node tail; // 该双向链表的尾节点,删除节点从尾部删除,表示最久访问int capacity;public DoublyLinkedList() {capacity=0; // 初始化容量为 0head = new Node();tail = new Node();head.post = tail;tail.pre = head;}int size(){return capacity;}Node peekFirst() { // 返回首结点不删除if (head.post != tail)return head.post;return null;}Node pollFirst() { // 返回首结点删除Node node=null;if (head.post != tail){node = new Node(head.post.key,head.post.value,head.post.freq);remove(head.post);}return node;}Node peekLast() {if (tail.pre != head)return tail.pre;return null;}Node pollLast() {Node node=null;if (tail.pre != head){node = new Node(tail.pre.key,tail.pre.value,tail.pre.freq);remove(tail.pre);}return node;}void remove(Node node) {if(node==null||capacity==0) return;node.pre.post = node.post;node.post.pre = node.pre;capacity--;}// 头插法void addNode(Node node) {node.post = head.post;head.post.pre = node;head.post = node;node.pre = head;capacity++;}}class LFUCache {int minfreq, capacity;Map<Integer, Node> key_table;Map<Integer, DoublyLinkedList> freq_table;public LFUCache(int capacity) {this.minfreq = 0;this.capacity = capacity;key_table = new HashMap<Integer, Node>(); // 维护O(1)查找、插入freq_table = new HashMap<Integer, DoublyLinkedList>(); // 维护删除过时结构}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.value, freq = node.freq;freq_table.get(freq).remove(node); // 在O(1)内删除结点// 如果当前链表为空,我们需要在哈希表中删除,且更新minFreqif (freq_table.get(freq).size() == 0) {freq_table.remove(freq);if (minfreq == freq) {minfreq++;}}// 插入到 freq + 1 中DoublyLinkedList list = freq_table.getOrDefault(freq + 1, new DoublyLinkedList());list.addNode(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);}}DoublyLinkedList list = freq_table.getOrDefault(1, new DoublyLinkedList());list.addNode(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++;}}DoublyLinkedList list = freq_table.getOrDefault(freq + 1, new DoublyLinkedList());list.addNode(new Node(key, value, freq + 1));freq_table.put(freq + 1, list);key_table.put(key, freq_table.get(freq + 1).peekFirst());}}}
