Skip to main content

Command Palette

Search for a command to run...

Solving Leetcode Interviews in Seconds with AI: LFU Cache

Updated
5 min read

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_freq variable 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 get and put operations.
    • Space Complexity: O(capacity), where capacity is the maximum number of key-value pairs the cache can store.

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)

More from this blog

C

Chatmagic blog

2894 posts

Solving Leetcode Interviews in Seconds with AI: LFU Cache