Description
Design and implement a data structure for Least Frequently Used (LFU) cache. It should support the following operations: get
and put
.
get(key)
- Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
put(key, value)
- Set or insert the value if the key is not already present. When the cache reaches its capacity, it should invalidate the least frequently used item before inserting a new item. For the purpose of this problem, when there is a tie (i.e., two or more keys that have the same frequency), the least recently used key would be evicted.
Follow up:
Could you do both operations in O(1) time complexity?
Example:
LFUCache cache = new LFUCache( 2 /* capacity */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // returns 1
cache.put(3, 3); // evicts key 2
cache.get(2); // returns -1 (not found)
cache.get(3); // returns 3.
cache.put(4, 4); // evicts key 1.
cache.get(1); // returns -1 (not found)
cache.get(3); // returns 3
cache.get(4); // returns 4
Solution
Two HashMap + DoublyLinkedList, O(1), S(capacity)
真的是难题啊,逻辑算很复杂的了,非常容易出bug。
思路基于LRU Cache,为了支持O(1) evict lease frequently used element,需要有一个freqMap
一个比较tricky的地方是,minFreq的更新好像很麻烦。但实际上没有想象中的复杂,minFreq的更新是有迹可循的。
- 当update node时,要判断node是否已经存在:
- 如果node存在,node.freq++,如果node.oldFreq == minFreq,可能需要删除minFreqList,这时只需判断minFreqList在更新之后是否为空即可。
- 不为空:minFreq保持不变
- 为空:minFreq++即可,因为node.newFreq = node.oldFreq + 1,且它一定变成了新的minFreq
- 如果node不存在,那么需要新建node,node.freq = 1。minFreq自然就变成1了,因为1是可能出现的最小freq!
- 如果node存在,node.freq++,如果node.oldFreq == minFreq,可能需要删除minFreqList,这时只需判断minFreqList在更新之后是否为空即可。
- 当evict node时,感觉minFreq的更新会变得复杂是不是,因为可能需要找到secondMinFreq对吗?但实际情况很简单,因为evict node一定是跟initialize node成对儿出现的(只有在达到capacity时),所以可以回归到上面的update node node为空的情况,minFreq一定会变成1。刺不刺激?
注意以下几个坑:
- Node或List的创建或销毁,一定要跟Map的更新同步操作!否则会出现不一致。
- 注意处理capacity == 0的情况
class LFUCache {
class Node {
Node prev, next;
int key, val, freq;
public Node(int k, int v) {
key = k;
val = v;
freq = 1; // initial freq is always 1
}
}
class DLL {
Node head, tail;
int size;
public DLL() {
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
}
// add node after head
public void add(Node node) {
head.next.prev = node;
node.next = head.next;
node.prev = head;
head.next = node;
++size;
}
public void remove(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
--size;
}
public Node removeLast() {
Node last = tail.prev;
remove(last);
return last;
}
public boolean isEmpty() {
return size == 0;
}
}
private int capacity;
private int minFreq;
private Map nodeMap;
private Map freqMap;
public LFUCache(int capacity) {
this.capacity = capacity;
minFreq = 0; // initial minFreq is 0 of course
nodeMap = new HashMap<>();
freqMap = new HashMap<>();
}
public int get(int key) {
if (!nodeMap.containsKey(key)) {
return -1;
}
Node node = nodeMap.get(key);
update(node);
return node.val;
}
public void put(int key, int val) {
if (capacity < 1) { // if capacity is 0, we shouldn't make any change
return;
}
if (nodeMap.containsKey(key)) {
Node node = nodeMap.get(key);
node.val = val;
update(node);
} else {
if (nodeMap.size() == capacity) {
evict();
}
Node node = new Node(key, val);
nodeMap.put(key, node);
minFreq = 1; // update minFreq to 1! Because curr node is a new one
DLL newList = freqMap.getOrDefault(node.freq, new DLL());
freqMap.put(node.freq, newList);
newList.add(node);
}
}
// handle the tricky logic to move node from oldFreqList to newFreqList
// don't forget to update minFreq if old freq list is the minFreq and empty
private void update(Node node) {
// deal with old freq list
int oldFreq = node.freq;
DLL oldList = freqMap.get(oldFreq);
oldList.remove(node);
if (oldList.isEmpty()) { // evict empty freq list
freqMap.remove(oldFreq);
if (oldFreq == minFreq) { // if the removed freq list is the min
++minFreq; // just increase the minFreq because freqs are consequtive!
}
}
// deal with new freq list
++node.freq;
DLL newList = freqMap.getOrDefault(node.freq, new DLL());
freqMap.put(node.freq, newList);
newList.add(node);
}
// evict lease frequently used node
private void evict() {
Node last = freqMap.get(minFreq).removeLast();
nodeMap.remove(last.key);
}
}
/**
* 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);
*/
Three HashMap + LinkedHashSet, O(1), S(capacity)
在Java中可以利用LinkedHashSet来当做DoublyLinkedList。
A LinkedHashSet is an ordered version of HashSet that maintains a doubly-linked List across all elements. When the iteration order is needed to be maintained this class in used. When iterating through a HashSet the order is unpredictable, while a LinkedHashSet lets us iterate through the elements in the order in which they were inserted.when cycling through LinkedHashSet using an iterator, the elements will be returned in the order in which they were inserted.
LinkedHashSet内部应该是一个HashMap + DoublyLinkedList,其特性为:
- 包含HashSet的一切特性:unique value,O(1) get, put
- remains insertion order:用iterator遍历时会按照插入顺序从老到新。
下面的代码就是用LinkedHashSet作为DLL的实现,思路跟上面的方案是一致的。
class LFUCache {
private int capacity;
private int minFreq;
private Map valMap;
private Map freqMap;
private Map> freqSetMap;
public LFUCache(int capacity) {
this.capacity = capacity;
minFreq = 0;
valMap = new HashMap<>();
freqMap = new HashMap<>();
freqSetMap = new HashMap<>();
}
public int get(int key) {
if (!valMap.containsKey(key)) {
return -1;
}
update(key);
return valMap.get(key);
}
public void put(int key, int val) {
if (capacity < 1) {
return;
}
if (valMap.containsKey(key)) {
valMap.put(key, val);
update(key);
} else {
if (valMap.size() == capacity) {
evict();
}
valMap.put(key, val);
freqMap.put(key, 1);
freqSetMap.put(1, freqSetMap.getOrDefault(1, new LinkedHashSet<>()));
freqSetMap.get(1).add(key);
minFreq = 1;
}
}
private void update(int key) {
int oldFreq = freqMap.get(key);
int newFreq = oldFreq + 1;
freqMap.put(key, newFreq);
Set oldFreqSet = freqSetMap.get(oldFreq);
oldFreqSet.remove(key);
if (oldFreqSet.isEmpty()) {
freqSetMap.remove(oldFreq);
if (oldFreq == minFreq) {
++minFreq;
}
}
Set newFreqSet = freqSetMap.getOrDefault(newFreq, new LinkedHashSet<>());
freqSetMap.put(newFreq, newFreqSet);
newFreqSet.add(key);
}
private void evict() {
Set minFreqSet = freqSetMap.get(minFreq);
int lastKey = minFreqSet.iterator().next();
minFreqSet.remove(lastKey);
valMap.remove(lastKey);
freqMap.remove(lastKey);
if (minFreqSet.isEmpty()) {
freqSetMap.remove(minFreq); // minFreq should be set to 1 later
}
}
}
/**
* 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);
*/