0460-lfu-cache

Try it on leetcode

Description

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.

 

 

Solution(Python)

#  LFU cache
#   Goal -> cache
#   eviction policy -> least frequenctly used
#  get  (key) -> value
#  put (key, value) updates the value if present or createds 
#       when cache reaches its capacity in invalidate the least frequenctlt used key before inserting new item
#  in tie -<> key  a == b then freq is compared
# 
# sample testcase
# cnt(x) 
# cache = []
# Lfu cache(2) cache = [_,_]
# p 1,1   cache = [1,_]  cnt(1) = 1
# p 2,2   cache = [2,1]  cnt(2) = 1, cnt(1)
# g 1   return 1
#       cache = [1,2] cnt(2) = 1,cnt(1) = 2
# p 3,3   cache = [3,1] cnt(1) = 2, cnt(3) = 1
# 
# g 2 return -1
# g 3 return 3  cache = [3,1] cnt(3) = 2, cnt(1) = 2
# p 4      here 1 is LRU  cache = [4,3] cnt(4) = 1, cnt(3) = 2
# g 1  return -1
# g 3   cache = [3,4] cnt(4) = 1 cnt(3) = 3
# g 4 cache =[4,3] cnt(4) = 2, cnt(3) = 3
# 
#  > cache should maintain relative order whenever key is accessed > linkedlist cache -> least used ...most used
#  > key to value,counter -> hashmap
#  > lfucacheline -> tracks least recently used and at back
#  
#  edge cases 
#  eviction strategy -> pop thefront element
#  cache retreieval
#  
#  get(key)
#    # if not found return -1
#    # if key found
#    increment the counter 
#   add to cacheline
#    # return from cache
#
#  put 
#     if cursize + 1 >= size : limit exceeds 
#           evict the least frequenctly used cache
#           
#     increment the counter 
#     create value in cache if not found
#       # insitalize counter to 1
#   add to cacheline
#     update cache if already found
# 
#  Space complexity: O(n)
#  Time Complexity: O(1)
#  
# Notes:
#  To efficiently track two things: frequency counts and order of access within frequency group
#   group nodes by frequency
#   within each group maintain order of access
#   Hashmap + doubly linkedlist gives O(1) time
#   which leads to:
#     A hahsmap -> key  == value
#    freqmap  -> freq == dll of nodes 
#       in doubly linkedlist maintain nodes in lru order 
#   min_freq -> minimum frequency level
#   
#   when a node is accessed :
#     remove it from the freqmap > if freqlist becomes empty it was minimum frequency , incrment min_freq
#   Add the node to the frequency + 1 list 
#   Always add to the head of the new list 
# 
from collections import defaultdict
from typing import Optional


class Node:
    """Node for doubly linked list to store cache entries."""
  
    def __init__(self, key: int, value: int) -> None:
        self.key = key
        self.value = value
        self.freq = 1  # Frequency counter starts at 1
        self.prev: Optional[Node] = None
        self.next: Optional[Node] = None


class DoublyLinkedList:
    """Doubly linked list to maintain nodes with same frequency in LRU order."""
  
    def __init__(self) -> None:
        # Create dummy head and tail nodes for easier manipulation
        self.head = Node(-1, -1)
        self.tail = Node(-1, -1)
        self.head.next = self.tail
        self.tail.prev = self.head

    def add_first(self, node: Node) -> None:
        """Add node right after head (most recently used position)."""
        node.prev = self.head
        node.next = self.head.next
        self.head.next.prev = node
        self.head.next = node

    def remove(self, node: Node) -> Node:
        """Remove a specific node from the list."""
        node.next.prev = node.prev
        node.prev.next = node.next
        node.next = None
        node.prev = None
        return node

    def remove_last(self) -> Node:
        """Remove and return the least recently used node (before tail)."""
        return self.remove(self.tail.prev)

    def is_empty(self) -> bool:
        """Check if the list is empty (only contains dummy nodes)."""
        return self.head.next == self.tail


class LFUCache:
    """
    Least Frequently Used (LFU) cache implementation.
    Evicts least frequently used items when capacity is reached.
    For items with same frequency, evicts least recently used.
    """
  
    def __init__(self, capacity: int) -> None:
        self.capacity = capacity
        self.min_freq = 0  # Track minimum frequency for eviction
        self.node_map: dict[int, Node] = {}  # Map from key to node
        self.freq_map: defaultdict[int, DoublyLinkedList] = defaultdict(DoublyLinkedList)  # Map from frequency to list of nodes

    def get(self, key: int) -> int:
        """
        Get value for the given key.
        Returns -1 if key doesn't exist.
        """
        # Handle edge cases
        if self.capacity == 0 or key not in self.node_map:
            return -1
      
        # Get node and update its frequency
        node = self.node_map[key]
        self._increment_frequency(node)
        return node.value

    def put(self, key: int, value: int) -> None:
        """
        Insert or update key-value pair in cache.
        Evicts LFU item if cache is at capacity.
        """
        # Handle zero capacity
        if self.capacity == 0:
            return
      
        # Update existing key
        if key in self.node_map:
            node = self.node_map[key]
            node.value = value
            self._increment_frequency(node)
            return
      
        # Evict LFU item if at capacity
        if len(self.node_map) == self.capacity:
            freq_list = self.freq_map[self.min_freq]
            evicted_node = freq_list.remove_last()
            del self.node_map[evicted_node.key]
      
        # Add new node
        node = Node(key, value)
        self._add_node(node)
        self.node_map[key] = node
        self.min_freq = 1  # New node has frequency 1

    def _increment_frequency(self, node: Node) -> None:
        """
        Increment frequency of a node and move it to appropriate frequency list.
        """
        freq = node.freq
        freq_list = self.freq_map[freq]
      
        # Remove node from current frequency list
        freq_list.remove(node)
      
        # Clean up empty frequency list and update min_freq if needed
        if freq_list.is_empty():
            del self.freq_map[freq]
            if freq == self.min_freq:
                self.min_freq += 1
      
        # Increment frequency and add to new frequency list
        node.freq += 1
        self._add_node(node)

    def _add_node(self, node: Node) -> None:
        """
        Add node to the frequency list corresponding to its frequency.
        """
        freq = node.freq
        freq_list = self.freq_map[freq]
        freq_list.add_first(node)


# Your LFUCache object will be instantiated and called as such:
# obj = LFUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key, value)


# Your LFUCache object will be instantiated and called as such:
# obj = LFUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)