Problem
Design and implement a data structure for a Least Frequently Used (LFU) cache.
Implement the LFUCache
class:
LFUCache(int capacity)
Initializes the object with thecapacity
of the data structure.int get(int key)
Gets the value of thekey
if thekey
exists in the cache. Otherwise, returns-1
.void put(int key, int value)
Update the value of thekey
if present, or inserts thekey
if not already present. When the cache reaches itscapacity
, it should invalidate and remove the least frequently used key before inserting a new item. For this problem, when there is a tie (i.e., two or more keys with the same frequency), the least recently usedkey
would be invalidated.
To determine the least frequently used key, a use counter is maintained for each key in the cache. The key with the smallest use counter is the least frequently used key.
When a key is first inserted into the cache, its use counter is set to 1
(due to the put
operation). The use counter for a key in the cache is incremented either a get
or put
operation is called on it.
The functions get
and put
must each run in O(1)
average time complexity.
Examples
Example 1:
Input:
["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]]
Output:
[null, null, null, 1, null, -1, 3, null, -1, 3, 4]
Explanation:
// cnt(x) = the use counter for key x
// cache=[] will show the last used order for tiebreakers (leftmost element is most recent)
LFUCache lfu = new LFUCache(2);
lfu.put(1, 1); // cache=[1,_], cnt(1)=1
lfu.put(2, 2); // cache=[2,1], cnt(2)=1, cnt(1)=1
lfu.get(1); // return 1
// cache=[1,2], cnt(2)=1, cnt(1)=2
lfu.put(3, 3); // 2 is the LFU key because cnt(2)=1 is the smallest, invalidate 2.
// cache=[3,1], cnt(3)=1, cnt(1)=2
lfu.get(2); // return -1 (not found)
lfu.get(3); // return 3
// cache=[3,1], cnt(3)=2, cnt(1)=2
lfu.put(4, 4); // Both 1 and 3 have the same cnt, but 1 is LRU, invalidate 1.
// cache=[4,3], cnt(4)=1, cnt(3)=2
lfu.get(1); // return -1 (not found)
lfu.get(3); // return 3
// cache=[3,4], cnt(4)=1, cnt(3)=3
lfu.get(4); // return 4
// cache=[4,3], cnt(4)=2, cnt(3)=3
Solution
Method 1 - PriorityQueue and HashMap
Code
class LFUCache {
long stamp;
int capacity;
int num;
PriorityQueue<Pair> minHeap;
HashMap<Integer, Pair> hashMap;
// @param capacity, an integer
public LFUCache(int capacity) {
// Write your code here
this.capacity = capacity;
num = 0;
minHeap = new PriorityQueue<Pair>();
hashMap = new HashMap<Integer, Pair>();
stamp = 0;
}
// @param key, an integer
// @param value, an integer
// @return nothing
public void set(int key, int value) {
if (capacity == 0) {
return;
}
// Write your code here
if (hashMap.containsKey(key)) {
Pair old = hashMap.get(key);
minHeap.remove(old);
Pair newNode = new Pair(key, value, old.times + 1, stamp++);
hashMap.put(key, newNode);
minHeap.offer(newNode);
} else if (num == capacity) {
Pair old = minHeap.poll();
hashMap.remove(old.key);
Pair newNode = new Pair(key, value, 1, stamp++);
hashMap.put(key, newNode);
minHeap.offer(newNode);
} else {
num++;
Pair pair = new Pair(key, value, 1, stamp++);
hashMap.put(key, pair);
minHeap.offer(pair);
}
}
public int get(int key) {
if (capacity == 0) {
return -1;
}
// Write your code here
if (hashMap.containsKey(key)) {
Pair old = hashMap.get(key);
minHeap.remove(old);
Pair newNode = new Pair(key, old.value, old.times + 1, stamp++);
hashMap.put(key, newNode);
minHeap.offer(newNode);
return hashMap.get(key).value;
} else {
return -1;
}
}
class Pair implements Comparable<Pair> {
long stamp;
int key;
int value;
int times;
public Pair(int key, int value, int times, long stamp) {
this.key = key;
this.value = value;
this.times = times;
this.stamp = stamp;
}
public int compareTo(Pair that) {
if (this.times == that.times) {
return (int)(this.stamp - that.stamp);
} else {
return this.times - that.times;
}
}
}
}
Complexity
- Time: get
O(n)
wheren
is capacity, and heap remove takes O(n) time. - Space:
O(capacity)
Method 2 - Using 3 Hashmaps and LinkedHashSet
Code
class LFUCache {
private final Map<Integer, Integer> vals;
private final Map<Integer, Integer> counts; // count of get's
private final Map<Integer, LinkedHashSet<Integer>> freqToValList;
private final int capacity;
private int min = -1;
public LFUCache(int capacity) {
this.capacity = capacity;
vals = new HashMap<>();
counts = new HashMap<>();
freqToValList = new HashMap<>();
freqToValList.put(1, new LinkedHashSet<>());
}
public int get(int key) {
if (!vals.containsKey(key)) {
return -1;
}
int count = counts.get(key);
counts.put(key, count + 1);
freqToValList.get(count).remove(key);
if (count == min && freqToValList.get(count).size() == 0) {
min++;
}
freqToValList.putIfAbsent(count + 1, new LinkedHashSet<>());
freqToValList.get(count + 1).add(key);
return vals.get(key);
}
public void put(int key, int value) {
if (capacity <= 0) {
return;
}
if (vals.containsKey(key)) {
vals.put(key, value);
get(key);
return;
}
if (vals.size() >= capacity) {
int evicted = freqToValList.get(min).iterator().next();
freqToValList.get(min).remove(evicted);
vals.remove(evicted);
}
vals.put(key, value);
counts.put(key, 1);
min = 1;
freqToValList.get(1).add(key);
}
}
Complexity
- Time: set
O(capacity)
getO(capacity)
- Space:
O(capacity)