Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions datastructures/lfucache/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# LFU Cache

Design and implement a data structure for a Least Frequently Used (LFU) cache.

Implement the LFUCache class:
Expand Down
64 changes: 61 additions & 3 deletions datastructures/lrucache/README.md
Original file line number Diff line number Diff line change
@@ -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
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.
35 changes: 16 additions & 19 deletions datastructures/lrucache/with_internal_linked_list.py
Original file line number Diff line number Diff line change
@@ -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
Comment on lines 5 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against zero/negative capacity to avoid sentinel eviction.

With capacity <= 0, put() falls into eviction logic and attempts to delete the sentinel (tail.previous), which can raise KeyError/AttributeError. A simple validation in __init__ avoids this class of bugs.

🛠️ Suggested fix
 def __init__(self, capacity: int):
+    if capacity <= 0:
+        raise ValueError("capacity must be a positive integer")
     self.capacity = capacity
     self.lookup: Dict[str | int, DoubleNode] = {}
     self.size = 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
def __init__(self, capacity: int):
if capacity <= 0:
raise ValueError("capacity must be a positive integer")
self.capacity = capacity
self.lookup: Dict[str | int, DoubleNode] = {}
self.size = 0
# 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.previous = self.head
🤖 Prompt for AI Agents
In `@datastructures/lrucache/with_internal_linked_list.py` around lines 5 - 14,
The constructor (__init__) doesn't guard against capacity <= 0 which allows
put() to trigger eviction of sentinel nodes (tail.previous) and crash; update
the __init__ (the constructor that sets self.capacity, self.lookup, self.head,
self.tail and uses DoubleNode) to validate capacity (e.g., raise ValueError for
capacity < 1 or coerce to a minimum of 1) so negative or zero capacities are
rejected before any put()/eviction logic touches head/tail/lookup.


@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)
Loading