Skip to content

Commit 1acda91

Browse files
authored
Merge pull request #167 from BrianLusina/feat/datastructures-lru-cache
refactor(datastructures, lru-cache): refactor lru cache
2 parents 4e58c1c + 991c2a7 commit 1acda91

3 files changed

Lines changed: 79 additions & 22 deletions

File tree

datastructures/lfucache/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# LFU Cache
2+
13
Design and implement a data structure for a Least Frequently Used (LFU) cache.
24

35
Implement the LFUCache class:

datastructures/lrucache/README.md

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,67 @@
1-
Design an LRU cache
1+
# Design an LRU cache
2+
3+
## Constraints and assumptions
24

3-
Constraints and assumptions
45
- What are we caching?
56
We are cahing the results of web queries
67
- Can we assume inputs are valid or do we have to validate them?
78
Assume they're valid
89
- Can we assume this fits memory?
9-
Yes
10+
Yes
11+
12+
## Solution
13+
14+
A hash map alone gives us `O(1)` key-value lookup, but doesn't track order. An array tracks order, but inserting/removing
15+
from the middle is `O(n)`. We need a data structure that supports `O(1)` insertion, deletion, AND reordering. This
16+
points us toward a structure where we can move items to the front/back instantly. Think of a playlist where songs move
17+
to the top when played: - When you play a song (access), it jumps to position #1 - When you add a new song and the
18+
playlist is full, the song at the bottom (least recently played) gets removed - You need to find any song by name
19+
instantly (hash map), but also know which song is at the bottom (ordering) This dual requirement - fast lookup by key
20+
AND fast reordering - suggests combining two data structures: one for `O(1)` key access, another for `O(1)` position
21+
changes.
22+
23+
### HashMap + Doubly Linked List hybrid
24+
25+
When you need O(1) access AND O(1) ordering operations (move to front/back, remove),
26+
combine a hash map for lookups with a doubly linked list for order tracking. The hash map stores pointers to list nodes,
27+
enabling instant node location and manipulation.
28+
29+
### Sentinel nodes eliminate edge cases
30+
31+
Use dummy head and tail nodes in your doubly linked list to avoid null checks when adding/removing nodes at boundaries.
32+
This means every real node always has non-null prev/next pointers, simplifying insertion and deletion logic dramatically.
33+
34+
### Access equals update pattern:
35+
36+
In LRU cache, every get() operation must update recency by moving the accessed node to the most-recent position
37+
(typically the head or tail). Forgetting this is the most common bug - reads aren't passive in cache implementations.
38+
39+
### Capacity check timing matters
40+
41+
Always check capacity and evict after inserting the new element, not before. For updates (key exists), no eviction is
42+
needed. For new insertions at capacity, evict the LRU item, then add - this handles the edge case where capacity=1
43+
correctly.
44+
45+
### Bidirectional pointer maintenance
46+
47+
When manipulating doubly linked list nodes, always update four pointers in the correct order: the node's prev/next AND
48+
its neighbors' pointers. A common pattern is to extract a node (reconnect its neighbors), then insert it elsewhere
49+
(update new neighbors and the node itself).
50+
51+
### Cache eviction policy abstraction
52+
53+
This LRU pattern extends to LFU (Least Frequently Used), MRU (Most Recently Used), and TTL caches. The core insight -
54+
combining hash map for O(1) lookup with an auxiliary structure (list, heap, or multiple lists) for O(1) policy
55+
enforcement - applies broadly to cache replacement algorithms.
56+
57+
## Complexity Analysis
58+
59+
### Time Complexity
60+
61+
O(1) Both get and put operations involve hash map lookup (O(1)), and linked list node
62+
insertion/deletion/movement (O(1) with doubly linked list). No iteration through the cache is needed.
63+
64+
### Space Complexity
65+
66+
O(capacity) We store at most 'capacity' key-value pairs in the hash map, and the same number of nodes in the doubly
67+
linked list. Space grows linearly with capacity.
Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,51 @@
1-
class Node:
2-
def __init__(self, key=0, data=0):
3-
self.data = data
4-
self.key = key
5-
self.prev = None
6-
self.next = None
7-
1+
from typing import Dict, Optional, Any
2+
from datastructures.linked_lists.doubly_linked_list.node import DoubleNode
83

94
class LRUCache:
105
def __init__(self, capacity: int):
116
self.capacity = capacity
12-
self.lookup = {}
7+
self.lookup: Dict[str | int, DoubleNode] = {}
138
self.size = 0
14-
self.head = Node()
15-
self.tail = Node()
9+
# Using sentinel head and tail nodes avoids null checks when adding/removing nodes at boundaries. This means
10+
# every real node always has non-null prev/next pointers, simplifying insertion and deletion logic dramatically
11+
self.head = DoubleNode(0)
12+
self.tail = DoubleNode(0)
1613
self.head.next = self.tail
17-
self.tail.prev = self.head
14+
self.tail.previous = self.head
1815

1916
@staticmethod
20-
def __delete_node(node):
17+
def __delete_node(node: DoubleNode):
2118
node.previous.next = node.next
2219
node.next.previous = node.previous
2320

24-
def __add_to_head(self, node):
21+
def __add_to_head(self, node: DoubleNode):
2522
node.next = self.head.next
2623
node.next.previous = node
2724
node.previous = self.head
2825
self.head.next = node
2926

30-
def get(self, key: int) -> int:
27+
def get(self, key: str | int) -> Optional[Any]:
3128
if key in self.lookup:
3229
node = self.lookup[key]
3330
data = node.data
3431
self.__delete_node(node)
3532
self.__add_to_head(node)
3633
return data
37-
return -1
34+
return None
3835

39-
def put(self, key: int, value: int) -> None:
36+
def put(self, key: str | int, value: int) -> None:
4037
if key in self.lookup:
4138
node = self.lookup[key]
4239
node.data = value
4340
self.__delete_node(node)
4441
self.__add_to_head(node)
4542
else:
46-
node = Node(key, value)
43+
node = DoubleNode(key=key, data=value)
4744
self.lookup[key] = node
4845
if self.size < self.capacity:
4946
self.size += 1
5047
self.__add_to_head(node)
5148
else:
52-
del self.lookup[self.tail.prev.key]
53-
self.__delete_node(self.tail.prev)
49+
del self.lookup[self.tail.previous.key]
50+
self.__delete_node(self.tail.previous)
5451
self.__add_to_head(node)

0 commit comments

Comments
 (0)