题目来源:https://leetcode.cn/problems/lfu-cache/
大致题意:
设计一个 LFU 缓存类:
- LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象
- int get(int key) - 如果键 key 存在于缓存中,则获取键的值,否则返回 - 1
- void put(int key, int value) - 如果键 key 已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量 capacity 时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键
思路
使用 TreeMap 维护一个 key 为使用次数,value 为键值对的有序集合
那么每次缓存放满后,从使用次数最小的键值对中删除最近最久未使用的键值对
具体的:
- 使用一个 countMap 维护使用次数与对应键值对的有序集合
- 设计一个双向链表,链表节点内存入键值对
- 使用一个 keyMap 维护当前已有的键值对与对应的使用次数
- 使用一个 nodeMap 维护当前已有的键值对与对应的链表节点
那么对于 get(int key) 方法:
- 首先判断 nodeMap 中是否有 key 对应的键值对,若没有则返回 -1;若有,进入下一步
- 获取键值对的使用次数,从原使用次数对应的链表中删去该节点(删除后若链表为空,则在 countMap 中删除该使用次数),然后在新使用次数链表中插入该键值对
- 更新该键值对的使用次数
对于 put(int key, int value) 方法:
- 首先判断 nodeMap 中是否有 key 对应的键值对,若没有则进入下一步;若有,则从 nodeMap 中获取该键值对对应的 node,更新 value,然后更新该键值对的使用次数(从原使用次数链表中删除,插入新使用次数链表,使用次数++)
- 判断容量是否为 0,为 0 直接返回;不为 0 判断容量是否已满,若满了删除使用次数最小对应的键值对链表中最近最久未使用的键值对(删除后若链表为空,删除该使用次数),从 keyMap、nodeMap 中移除;若未满进入下一步
- 创建 node 节点,存入键值对,将其插入 keyMap、nodeMap 和 使用次数为 1 的链表
class LFUCache {
TreeMap<Integer, Node> countMap; // key 为每种使用次数,value 为对应的键的双向链表
Map<Integer, Integer> map; // key 缓存中已有的键值对, value 为键值对对应的次数
Map<Integer, Node> nodeMap; // key 为缓存中键值对的键,value 为该键值对对应的 node
int capacity; // 容量
int size; // 当前存的键值对个数
public LFUCache(int capacity) {
countMap = new TreeMap<>();
map = new HashMap<>();
nodeMap = new HashMap<>();
this.capacity = capacity;
size = 0;
}
/**
* 获取 key 对应的键值对
*
* @param key
* @return
*/
public int get(int key) {
if (nodeMap.containsKey(key)) {
int count = keyMap.get(key); // 获取对应键值对使用次数
Node node = nodeMap.get(key); // 获取键值对对应的 node
// 使用次数 +1
addCount(key, count, node);
return node.val;
}
return -1;
}
/**
* 将键值对放入缓存
*
* @param key
* @param value
*/
public void put(int key, int value) {
if (nodeMap.containsKey(key)) {
int count = keyMap.get(key); // 获取对应键值对使用次数
Node node = nodeMap.get(key); // 获取键值对对应的 node
// 更新 node 值
node.val = value;
// 使用次数 +1
addCount(key, count, node);
} else {
if (capacity == 0) {
return;
}
Node node = new Node(key, value);
// 如果容量已经满了,移除最不经常使用的键值对
if (size == capacity) {
// 移除使用次数最少的链表中最不经常使用的键值对
// 获取使用次数最少的链表
Map.Entry<Integer, Node> firstEntry = countMap.entrySet().iterator().next();
// 获取链表
Node head = firstEntry.getValue();
// 移除最不经常使用的键值对
Node removeNode = head.pre;
remove(removeNode);
keyMap.remove(removeNode.key);
nodeMap.remove(removeNode.key);
// 如果链表为空,移除该链表
if (head.pre == head) {
countMap.remove(firstEntry.getKey());
}
size--;
}
size++;
// 获取使用次数为 1 的链表
Node head = countMap.getOrDefault(1, new Node(-1, -1));
// 加入该键值对
insert(head, node);
countMap.putIfAbsent(1, head);
nodeMap.put(key, node);
keyMap.put(key, 1);
}
}
/**
* 在键值对原来使用次数的键值对链表中删除该键值对,并插入新的使用次数对应链表
*
* @param key 键值对的 key
* @param count 键值对使用次数
* @param node 键值对所在 node
*/
public void addCount(int key, int count, Node node) {
remove(node); // 在旧链表中移除 node
// 判断旧链表是否为空
// 首先获取旧链表头节点
Node head = countMap.get(count);
// 判断是否为空
if (head.next == head) {
// 删除该链表
countMap.remove(count);
}
head = countMap.getOrDefault(count + 1, new Node(-1, -1)); // 获取使用次数 +1 对应的链表头节点
insert(head, node); // 将 node 插入新链表中
countMap.putIfAbsent(count + 1, head); // 将新链表放入哈希表
// 更新使用次数
keyMap.put(key, count + 1);
}
/**
* 在 head 对应的 LRU 链表中插入 node
*
* @param head 链表头节点
* @param node 待插入的节点
*/
public void insert(Node head, Node node) {
node.next = head.next;
head.next = node;
node.next.pre = node;
node.pre = head;
}
/**
* 从 node 原 LRU 链表中删除 node
*
* @param node
*/
public void remove(Node node) {
node.next.pre = node.pre;
node.pre.next = node.next;
}
class Node {
Node pre;
Node next;
int val;
int key;
public Node(int key, int val) {
this.key = key;
this.val = val;
this.pre = this;
this.next = this;
}
}
}
/**
* 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);
*/