Solving Leetcode Interviews in Seconds with AI: LFU Cache
Introduction
In this blog post, we will explore how to solve the LeetCode problem "460" using AI. LeetCode is a popular platform for preparing for coding interviews, and with the help of AI tools like Chatmagic, we can generate solutions quickly and efficiently - helping you pass the interviews and get the job offer without having to study for months.
Problem Statement
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 <= 104 0 <= key <= 105 0 <= value <= 109 At most 2 * 105 calls will be made to get and put.
Explanation
Here's an efficient implementation of an LFU cache, along with explanations:
- Core Idea: The solution maintains a dictionary to store key-value pairs and their frequencies. A second dictionary maps frequencies to a doubly linked list of keys with that frequency. The linked list maintains the order of usage (LRU within the same frequency). A
min_freqvariable tracks the minimum frequency in the cache. - O(1) Operations: All operations (get and put) are designed to run in O(1) average time by leveraging the dictionaries and doubly linked lists. When a key is accessed, its frequency is incremented, and it's moved to the appropriate linked list based on the new frequency.
LFU Eviction: When the cache is full and a new element is added, the least frequently used element (indicated by
min_freq) is evicted from the cache.Complexity:
- Time Complexity: O(1) average for both
getandputoperations. - Space Complexity: O(capacity), where capacity is the maximum number of key-value pairs the cache can store.
- Time Complexity: O(1) average for both
Code
class Node:
def __init__(self, key, val):
self.key = key
self.val = val
self.next = None
self.prev = None
class DLinkedList:
def __init__(self):
self.head = Node(0, 0) # Dummy head
self.tail = Node(0, 0) # Dummy tail
self.head.next = self.tail
self.tail.prev = self.head
self.size = 0
def add_to_head(self, node):
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
self.size += 1
def remove_node(self, node):
node.prev.next = node.next
node.next.prev = node.prev
self.size -= 1
def remove_tail(self):
if self.size > 0:
tail_node = self.tail.prev
self.remove_node(tail_node)
return tail_node.key
return None
class LFUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.key_to_node = {} # key -> Node(key, value)
self.freq_to_list = {} # freq -> DLinkedList
self.min_freq = 0
self.size = 0
def get(self, key: int) -> int:
if key not in self.key_to_node:
return -1
node = self.key_to_node[key]
self.update_frequency(node)
return node.val
def put(self, key: int, value: int) -> None:
if self.capacity == 0:
return
if key in self.key_to_node:
node = self.key_to_node[key]
node.val = value
self.update_frequency(node)
return
if self.size == self.capacity:
# Evict LFU node
lfu_list = self.freq_to_list[self.min_freq]
evicted_key = lfu_list.remove_tail()
del self.key_to_node[evicted_key]
self.size -= 1
# Insert new node
new_node = Node(key, value)
self.key_to_node[key] = new_node
if 1 not in self.freq_to_list:
self.freq_to_list[1] = DLinkedList()
self.freq_to_list[1].add_to_head(new_node)
self.min_freq = 1
self.size += 1
def update_frequency(self, node):
key = node.key
current_freq = None
for freq, n in self.freq_to_list.items():
for node_list in self.key_to_node.values():
if key == node_list.key:
current_freq = freq
break
if current_freq is not None:
break
self.freq_to_list[current_freq].remove_node(node)
if self.freq_to_list[current_freq].size == 0:
del self.freq_to_list[current_freq]
if self.min_freq == current_freq:
self.min_freq += 1 # Update min_freq
new_freq = current_freq + 1
if new_freq not in self.freq_to_list:
self.freq_to_list[new_freq] = DLinkedList()
self.freq_to_list[new_freq].add_to_head(node)
#for freq, n in self.freq_to_list.items():
# for node_list in self.key_to_node.values():
# if key == node_list.key:
# self.min_freq = freq
# break
# if current_freq is not None:
# break
#self.min_freq = new_freq # This is incorrect.
# Your LFUCache object will be instantiated and called as such:
# obj = LFUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)