Design and implement a data structure for a Least Frequently Used (LFU) cache.
Implement the LFUCache class:
LFUCache(int capacity) Initializes the object with the capacity of the data structure.int get(int key) Gets the value of the key if the key exists in the cache. Otherwise, returns -1.void put(int key, int value) Update the value of the key if present, or inserts the key if not already present. When the cache reaches its capacity, 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 used key 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.
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
Constraints:
1 <= capacity <= 10,000.0 <= key <= 100,0000 <= value <= 1,000,000,000200,000 calls will be made to get and put.Before attempting this problem, you should be comfortable with:
An LFU (Least Frequently Used) cache evicts the element that has been accessed the fewest times. When there's a tie in frequency, we evict the least recently used among them. The simplest approach stores each key's value, frequency, and a timestamp. On eviction, we scan all entries to find the one with the minimum frequency (and earliest timestamp for ties).
[value, frequency, timestamp] in a hash map.get(key): if the key exists, increment its frequency, update its timestamp, and return the value. Otherwise return -1.put(key, value): if the key exists, update its value, increment frequency, and update timestamp.1 and the current timestamp.class LFUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.cache = {} # key -> [value, frequency, timestamp]
self.timestamp = 0
def get(self, key: int) -> int:
if key not in self.cache:
return -1
self.cache[key][1] += 1
self.timestamp += 1
self.cache[key][2] = self.timestamp
return self.cache[key][0]
def put(self, key: int, value: int) -> None:
if self.capacity <= 0:
return
self.timestamp += 1
if key in self.cache:
self.cache[key][0] = value
self.cache[key][1] += 1
self.cache[key][2] = self.timestamp
return
if len(self.cache) >= self.capacity:
min_freq = float('inf')
min_timestamp = float('inf')
lfu_key = None
for k, (_, freq, ts) in self.cache.items():
if freq < min_freq or (freq == min_freq and ts < min_timestamp):
min_freq = freq
min_timestamp = ts
lfu_key = k
if lfu_key is not None:
del self.cache[lfu_key]
self.cache[key] = [value, 1, self.timestamp]To achieve O(1) operations, we need fast access to a key's current node and to the least recently used node inside each frequency group. We use a hash map from frequency to a doubly linked list of cache nodes, and a separate key-to-node map for direct access. Each linked list maintains recency order within one frequency bucket, so the head is the eviction candidate for ties. We also track the current minimum frequency to quickly find which list to evict from.
nodeMap (or equivalent key-to-node lookup) and a listMap (frequency to doubly linked list of nodes).lfuCount, the current minimum frequency in the cache.get(key): if the key exists, move it from its current frequency list to the next frequency list, update the frequency, and adjust lfuCount if needed.put(key, value): if the key exists, update its value and treat it like a get operation. If inserting a new key and at capacity, pop the leftmost (oldest) element from the lfuCount list.lfuCount to 1.class ListNode:
def __init__(self, key, val):
self.key = key
self.val = val
self.freq = 1
self.prev = None
self.next = None
class LinkedList:
def __init__(self):
self.left = ListNode(0, 0)
self.right = ListNode(0, 0)
self.left.next = self.right
self.right.prev = self.left
self.size = 0
def length(self):
return self.size
def pushRight(self, node):
prev = self.right.prev
prev.next = node
node.prev = prev
node.next = self.right
self.right.prev = node
self.size += 1
def pop(self, node):
prev, next = node.prev, node.next
prev.next = next
next.prev = prev
node.prev = None
node.next = None
self.size -= 1
def popLeft(self):
if self.length() == 0:
return None
node = self.left.next
self.pop(node)
return node
class LFUCache:
def __init__(self, capacity: int):
self.cap = capacity
self.lfuCnt = 0
self.nodeMap = {} # Map key -> node
# Map frequency -> linkedlist of nodes
self.listMap = defaultdict(LinkedList)
def counter(self, node):
cnt = node.freq
self.listMap[cnt].pop(node)
if cnt == self.lfuCnt and self.listMap[cnt].length() == 0:
self.lfuCnt += 1
node.freq += 1
self.listMap[node.freq].pushRight(node)
def get(self, key: int) -> int:
if key not in self.nodeMap:
return -1
node = self.nodeMap[key]
self.counter(node)
return node.val
def put(self, key: int, value: int) -> None:
if self.cap == 0:
return
if key in self.nodeMap:
node = self.nodeMap[key]
node.val = value
self.counter(node)
return
if len(self.nodeMap) == self.cap:
node = self.listMap[self.lfuCnt].popLeft()
self.nodeMap.pop(node.key)
node = ListNode(key, value)
self.nodeMap[key] = node
self.listMap[1].pushRight(node)
self.lfuCnt = 1When inserting a new key, the minimum frequency tracker must reflect that the new key starts at frequency 1. Failing to update lfuCount causes subsequent evictions to look in the wrong frequency bucket, potentially evicting the wrong item or causing errors.
Both get() and put() (when updating an existing key) should increment the frequency. A common mistake is only incrementing on get(), which causes keys that are repeatedly updated but never retrieved to have artificially low frequencies and get evicted prematurely.
When multiple keys share the same minimum frequency, the least recently used among them should be evicted. This requires maintaining insertion order within each frequency bucket. Using a regular set or unordered structure loses this ordering and leads to incorrect evictions.
When the cache capacity is 0, all put() operations should be no-ops since nothing can be stored. Forgetting this check causes attempts to evict from an empty cache, leading to errors or incorrect behavior.
When a key's frequency increases, its node must be removed from the current frequency list and added to the next one. Forgetting to remove it from the old list causes the same entry to appear in multiple frequency buckets, corrupting the data structure and causing incorrect evictions.
Sign in to join the discussion