diff --git a/datastructures/lfucache/README.md b/datastructures/lfucache/README.md index b4e9d34c..2b75c136 100644 --- a/datastructures/lfucache/README.md +++ b/datastructures/lfucache/README.md @@ -1,3 +1,5 @@ +# LFU Cache + Design and implement a data structure for a Least Frequently Used (LFU) cache. Implement the LFUCache class: diff --git a/datastructures/lrucache/README.md b/datastructures/lrucache/README.md index a5271ac9..ab071bdd 100644 --- a/datastructures/lrucache/README.md +++ b/datastructures/lrucache/README.md @@ -1,9 +1,67 @@ -Design an LRU cache +# Design an LRU cache + +## Constraints and assumptions -Constraints and assumptions - What are we caching? We are cahing the results of web queries - Can we assume inputs are valid or do we have to validate them? Assume they're valid - Can we assume this fits memory? - Yes \ No newline at end of file + Yes + +## Solution + +A hash map alone gives us `O(1)` key-value lookup, but doesn't track order. An array tracks order, but inserting/removing +from the middle is `O(n)`. We need a data structure that supports `O(1)` insertion, deletion, AND reordering. This +points us toward a structure where we can move items to the front/back instantly. Think of a playlist where songs move +to the top when played: - When you play a song (access), it jumps to position #1 - When you add a new song and the +playlist is full, the song at the bottom (least recently played) gets removed - You need to find any song by name +instantly (hash map), but also know which song is at the bottom (ordering) This dual requirement - fast lookup by key +AND fast reordering - suggests combining two data structures: one for `O(1)` key access, another for `O(1)` position +changes. + +### HashMap + Doubly Linked List hybrid + +When you need O(1) access AND O(1) ordering operations (move to front/back, remove), +combine a hash map for lookups with a doubly linked list for order tracking. The hash map stores pointers to list nodes, +enabling instant node location and manipulation. + +### Sentinel nodes eliminate edge cases + +Use dummy head and tail nodes in your doubly linked list to avoid null checks when adding/removing nodes at boundaries. +This means every real node always has non-null prev/next pointers, simplifying insertion and deletion logic dramatically. + +### Access equals update pattern: + +In LRU cache, every get() operation must update recency by moving the accessed node to the most-recent position +(typically the head or tail). Forgetting this is the most common bug - reads aren't passive in cache implementations. + +### Capacity check timing matters + +Always check capacity and evict after inserting the new element, not before. For updates (key exists), no eviction is +needed. For new insertions at capacity, evict the LRU item, then add - this handles the edge case where capacity=1 +correctly. + +### Bidirectional pointer maintenance + +When manipulating doubly linked list nodes, always update four pointers in the correct order: the node's prev/next AND +its neighbors' pointers. A common pattern is to extract a node (reconnect its neighbors), then insert it elsewhere +(update new neighbors and the node itself). + +### Cache eviction policy abstraction + +This LRU pattern extends to LFU (Least Frequently Used), MRU (Most Recently Used), and TTL caches. The core insight - +combining hash map for O(1) lookup with an auxiliary structure (list, heap, or multiple lists) for O(1) policy +enforcement - applies broadly to cache replacement algorithms. + +## Complexity Analysis + +### Time Complexity + +O(1) Both get and put operations involve hash map lookup (O(1)), and linked list node +insertion/deletion/movement (O(1) with doubly linked list). No iteration through the cache is needed. + +### Space Complexity + +O(capacity) We store at most 'capacity' key-value pairs in the hash map, and the same number of nodes in the doubly +linked list. Space grows linearly with capacity. diff --git a/datastructures/lrucache/with_internal_linked_list.py b/datastructures/lrucache/with_internal_linked_list.py index 29223c32..85853099 100644 --- a/datastructures/lrucache/with_internal_linked_list.py +++ b/datastructures/lrucache/with_internal_linked_list.py @@ -1,54 +1,51 @@ -class Node: - def __init__(self, key=0, data=0): - self.data = data - self.key = key - self.prev = None - self.next = None - +from typing import Dict, Optional, Any +from datastructures.linked_lists.doubly_linked_list.node import DoubleNode class LRUCache: def __init__(self, capacity: int): self.capacity = capacity - self.lookup = {} + self.lookup: Dict[str | int, DoubleNode] = {} self.size = 0 - self.head = Node() - self.tail = Node() + # Using sentinel head and tail nodes avoids null checks when adding/removing nodes at boundaries. This means + # every real node always has non-null prev/next pointers, simplifying insertion and deletion logic dramatically + self.head = DoubleNode(0) + self.tail = DoubleNode(0) self.head.next = self.tail - self.tail.prev = self.head + self.tail.previous = self.head @staticmethod - def __delete_node(node): + def __delete_node(node: DoubleNode): node.previous.next = node.next node.next.previous = node.previous - def __add_to_head(self, node): + def __add_to_head(self, node: DoubleNode): node.next = self.head.next node.next.previous = node node.previous = self.head self.head.next = node - def get(self, key: int) -> int: + def get(self, key: str | int) -> Optional[Any]: if key in self.lookup: node = self.lookup[key] data = node.data self.__delete_node(node) self.__add_to_head(node) return data - return -1 + return None - def put(self, key: int, value: int) -> None: + def put(self, key: str | int, value: int) -> None: if key in self.lookup: node = self.lookup[key] node.data = value self.__delete_node(node) self.__add_to_head(node) else: - node = Node(key, value) + node = DoubleNode(key=key, data=value) self.lookup[key] = node if self.size < self.capacity: self.size += 1 self.__add_to_head(node) else: - del self.lookup[self.tail.prev.key] - self.__delete_node(self.tail.prev) + del self.lookup[self.tail.previous.key] + self.__delete_node(self.tail.previous) self.__add_to_head(node)