From 3737e45fd6968265fcc45fb8615541a19aa9a64f Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 30 Jun 2026 14:25:10 +0100 Subject: [PATCH 01/10] feat: define Node class with key-value storage for LRU Cache - Add 'key' attribute to Node to facilitate dictionary cleanup during eviction - Maintain pointers for doubly linked list functionality --- Sprint-2/implement_lru_cache/lru_cache.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Sprint-2/implement_lru_cache/lru_cache.py b/Sprint-2/implement_lru_cache/lru_cache.py index e69de29..e40e695 100644 --- a/Sprint-2/implement_lru_cache/lru_cache.py +++ b/Sprint-2/implement_lru_cache/lru_cache.py @@ -0,0 +1,11 @@ +class Node: + """ + Step 1: Updated Node class. + Each node stores both key and value so we can find the + dictionary entry when a node is evicted from the tail. + """ + def __init__(self, key, value): + self.key = key + self.value = value + self.next = None + self.previous = None From a19e9bf096655efe78ca1a3e5fc694f21ef2c6d5 Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 30 Jun 2026 14:26:47 +0100 Subject: [PATCH 02/10] feat: add LinkedList class to manage cache usage order - Initialize head and tail pointers for the Doubly Linked List - Add documentation explaining MRU (head) and LRU (tail) logic --- Sprint-2/implement_lru_cache/lru_cache.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Sprint-2/implement_lru_cache/lru_cache.py b/Sprint-2/implement_lru_cache/lru_cache.py index e40e695..6688df4 100644 --- a/Sprint-2/implement_lru_cache/lru_cache.py +++ b/Sprint-2/implement_lru_cache/lru_cache.py @@ -9,3 +9,16 @@ def __init__(self, key, value): self.value = value self.next = None self.previous = None + + +class LinkedList: + """ + A Doubly Linked List to track the order of usage. + Head is Most Recently Used (MRU). + Tail is Least Recently Used (LRU). + """ + def __init__(self): + self.head = None + self.tail = None + + \ No newline at end of file From 51c5c710978cfb7a658940914660b09ddc8c7bc3 Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 30 Jun 2026 14:28:43 +0100 Subject: [PATCH 03/10] feat: implement push_head method for LinkedList - Add logic to insert nodes at the front of the list (Most Recently Used) - Ensure tail pointer is set correctly when adding to an empty list - Update previous/next pointers of existing head to maintain bi-directional links --- Sprint-2/implement_lru_cache/lru_cache.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Sprint-2/implement_lru_cache/lru_cache.py b/Sprint-2/implement_lru_cache/lru_cache.py index 6688df4..4070ab2 100644 --- a/Sprint-2/implement_lru_cache/lru_cache.py +++ b/Sprint-2/implement_lru_cache/lru_cache.py @@ -21,4 +21,18 @@ def __init__(self): self.head = None self.tail = None - \ No newline at end of file + def push_head(self, node): + """Adds an existing node to the front of the list.""" + node.next = self.head + node.previous = None + + if self.head is not None: + self.head.previous = node + + self.head = node + + # If list was empty, this node is also the tail + if self.tail is None: + self.tail = node + + \ No newline at end of file From 01c34eeff818b394eeb3fe47cc0ea84888d63465 Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 30 Jun 2026 14:30:49 +0100 Subject: [PATCH 04/10] feat: implement pop_tail method for LinkedList - Add logic to remove and return the least recently used (tail) node - Handle single-node list resets by clearing both head and tail pointers - Ensure the new tail's 'next' pointer is cleared to prevent memory leaks --- Sprint-2/implement_lru_cache/lru_cache.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Sprint-2/implement_lru_cache/lru_cache.py b/Sprint-2/implement_lru_cache/lru_cache.py index 4070ab2..d9c69f9 100644 --- a/Sprint-2/implement_lru_cache/lru_cache.py +++ b/Sprint-2/implement_lru_cache/lru_cache.py @@ -35,4 +35,18 @@ def push_head(self, node): if self.tail is None: self.tail = node - \ No newline at end of file + def pop_tail(self): + """Removes the last node (the oldest item) and returns it.""" + if self.tail is None: + return None + + old_tail = self.tail + + if self.head == self.tail: + self.head = None + self.tail = None + else: + self.tail = self.tail.previous + self.tail.next = None + + return old_tail From abaca899ba2f875f19004e09026dd33155c4f6ad Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 30 Jun 2026 14:33:01 +0100 Subject: [PATCH 05/10] feat: implement remove method for LinkedList - Add logic to disconnect a node from any position in the list - Update head and tail pointers when removing boundary nodes - Nullify removed node pointers to ensure a clean disconnection --- Sprint-2/implement_lru_cache/lru_cache.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Sprint-2/implement_lru_cache/lru_cache.py b/Sprint-2/implement_lru_cache/lru_cache.py index d9c69f9..a31c3a9 100644 --- a/Sprint-2/implement_lru_cache/lru_cache.py +++ b/Sprint-2/implement_lru_cache/lru_cache.py @@ -50,3 +50,19 @@ def pop_tail(self): self.tail.next = None return old_tail + + def remove(self, node): + """Unplucks a node from its current position in the list.""" + if node.previous is not None: + node.previous.next = node.next + else: + self.head = node.next + + if node.next is not None: + node.next.previous = node.previous + else: + self.tail = node.previous + + # Clean up pointers of the removed node + node.next = None + node.previous = None From 76218056c3b4579a519c0395528c4423edb726f3 Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 30 Jun 2026 14:38:00 +0100 Subject: [PATCH 06/10] feat: implement LruCache class and initialization logic - Add constructor with validation to ensure limit is at least 1 - Initialize dictionary for O(1) key-to-node lookups - Initialize LinkedList to track item access order --- Sprint-2/implement_lru_cache/lru_cache.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Sprint-2/implement_lru_cache/lru_cache.py b/Sprint-2/implement_lru_cache/lru_cache.py index a31c3a9..2bdb6ea 100644 --- a/Sprint-2/implement_lru_cache/lru_cache.py +++ b/Sprint-2/implement_lru_cache/lru_cache.py @@ -66,3 +66,23 @@ def remove(self, node): # Clean up pointers of the removed node node.next = None node.previous = None + + +class LruCache: + """ + The LRU Cache: Dictionary + Linked List. + Provides O(1) time complexity for both get and set operations. + """ + def __init__(self, limit): + # 1. Validation + if limit < 1: + raise ValueError("Cache limit must be at least 1.") + + # 2. Store the limit + self.limit = limit + + # 3. Dictionary for O(1) key lookups (Key -> Node) + self.lookup = {} + + # 4. Doubly Linked List for O(1) ordering + self.order = LinkedList() From e2d5a85e265e11d0497f958140d40334514096bf Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 30 Jun 2026 14:39:59 +0100 Subject: [PATCH 07/10] feat: implement get method for LruCache - Add O(1) lookup functionality using the dictionary - Implement move-to-front logic to mark accessed items as Most Recently Used - Return None for keys not present in the cache --- Sprint-2/implement_lru_cache/lru_cache.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Sprint-2/implement_lru_cache/lru_cache.py b/Sprint-2/implement_lru_cache/lru_cache.py index 2bdb6ea..e1a1b8d 100644 --- a/Sprint-2/implement_lru_cache/lru_cache.py +++ b/Sprint-2/implement_lru_cache/lru_cache.py @@ -86,3 +86,19 @@ def __init__(self, limit): # 4. Doubly Linked List for O(1) ordering self.order = LinkedList() + + def get(self, key): + """ + Logic for get(key): + Find the item, move it to the front, return value. + """ + if key not in self.lookup: + return None + + node = self.lookup[key] + + # Move to front (Most Recently Used) + self.order.remove(node) + self.order.push_head(node) + + return node.value From db3135896184cf33c25e8bb237c83fe959bf791a Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 30 Jun 2026 14:41:51 +0100 Subject: [PATCH 08/10] feat: implement set method with LRU eviction logic - Add logic to update existing keys and move them to the front (MRU) - Implement eviction of the Least Recently Used (tail) node when the limit is reached - Ensure the lookup dictionary and linked list stay synchronized during eviction - Add new nodes to both the dictionary and the front of the list --- Sprint-2/implement_lru_cache/lru_cache.py | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Sprint-2/implement_lru_cache/lru_cache.py b/Sprint-2/implement_lru_cache/lru_cache.py index e1a1b8d..61f1d49 100644 --- a/Sprint-2/implement_lru_cache/lru_cache.py +++ b/Sprint-2/implement_lru_cache/lru_cache.py @@ -102,3 +102,28 @@ def get(self, key): self.order.push_head(node) return node.value + + def set(self, key, value): + """ + Logic for set(key, value): + If exists: update and move to front. + If new: check limit, evict tail if full, then add to front. + """ + if key in self.lookup: + # Update existing + node = self.lookup[key] + node.value = value + self.order.remove(node) + self.order.push_head(node) + else: + # Check limit + if len(self.lookup) >= self.limit: + # Evict the oldest (tail) + evicted_node = self.order.pop_tail() + if evicted_node: + del self.lookup[evicted_node.key] + + # Create and add new node + new_node = Node(key, value) + self.order.push_head(new_node) + self.lookup[key] = new_node \ No newline at end of file From becdd11ac097f41960060b5707b60d099d70b6b5 Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 30 Jun 2026 14:46:57 +0100 Subject: [PATCH 09/10] test: add edge case and complex value tests for LruCache - Verify that updating existing keys correctly refreshes their MRU status - Test edge case for a cache with a limit of one - Add test for retrieving non-existent keys (returning None) - Confirm support for complex data types like lists and dictionaries --- .../implement_lru_cache/lru_cache_test.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Sprint-2/implement_lru_cache/lru_cache_test.py b/Sprint-2/implement_lru_cache/lru_cache_test.py index d37df01..3b8cd1d 100644 --- a/Sprint-2/implement_lru_cache/lru_cache_test.py +++ b/Sprint-2/implement_lru_cache/lru_cache_test.py @@ -54,6 +54,40 @@ def test_eviction_order_after_gets(self): self.assertEqual(cache.get("a"), 1) self.assertEqual(cache.get("c"), 3) + def test_update_existing_key(self): + cache = LruCache(limit=2) + cache.set("a", 1) + cache.set("b", 2) + + # Update "a" - it should stay in the cache but change value + cache.set("a", 100) + self.assertEqual(cache.get("a"), 100) + + # Adding "c" should now evict "b", because "a" was recently updated + cache.set("c", 3) + self.assertIsNone(cache.get("b")) + self.assertEqual(cache.get("a"), 100) + + def test_limit_of_one(self): + cache = LruCache(limit=1) + cache.set("a", 1) + self.assertEqual(cache.get("a"), 1) + + cache.set("b", 2) # This should immediately kick out "a" + self.assertIsNone(cache.get("a")) + self.assertEqual(cache.get("b"), 2) + + def test_get_non_existent_key(self): + cache = LruCache(limit=5) + # Testing "None" return for keys never added + self.assertIsNone(cache.get("missing_key")) + + def test_complex_values(self): + cache = LruCache(limit=2) + # Testing that we can store lists or dictionaries, not just strings + cache.set("list", [1, 2, 3]) + self.assertEqual(cache.get("list"), [1, 2, 3]) + if __name__ == "__main__": unittest.main() From f1260f07539164d486c06bb6c7a8cf9f1749993c Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 30 Jun 2026 14:54:44 +0100 Subject: [PATCH 10/10] docs: add detailed implementation notes for LRU Cache - Explain hybrid architecture (Dictionary + Doubly Linked List) - Document the "why" behind O(1) time complexity requirements - Detail the space-vs-time trade-off used in the design - Summarize testing strategy for edge cases and eviction logic --- Sprint-2/implement_lru_cache/CHANGE_MADE.md | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 Sprint-2/implement_lru_cache/CHANGE_MADE.md diff --git a/Sprint-2/implement_lru_cache/CHANGE_MADE.md b/Sprint-2/implement_lru_cache/CHANGE_MADE.md new file mode 100644 index 0000000..eabf467 --- /dev/null +++ b/Sprint-2/implement_lru_cache/CHANGE_MADE.md @@ -0,0 +1,45 @@ +# Changes Made: LRU Cache Implementation + +## 1. Architectural Overview +To achieve the requirement of **O(1) time complexity** for both `get` and `set` operations, I implemented a hybrid data structure combining a **Python Dictionary** and a **Doubly Linked List**. + +- **The Dictionary (`self.lookup`)**: Provides constant time O(1) access to any node using its key. +- **The Doubly Linked List (`self.order`)**: Maintains the "recency" of items. The **Head** represents the Most Recently Used (MRU) item, and the **Tail** represents the Least Recently Used (LRU) item. + +## 2. Component Breakdown + +### Node Class +- **Key-Value Storage**: Unlike a standard linked list node, these nodes store both the `key` and the `value`. +- **The "Why"**: Storing the `key` is essential during eviction. When we remove the tail node from the list, we must also delete its corresponding entry from the dictionary. The node must "know" its key so we can perform this reverse lookup. + +### LinkedList Class +- **Pointer Management**: Manages `head` and `tail` pointers. +- **Methods**: + - `push_head(node)`: Places a node at the front (MRU position). + - `pop_tail()`: Removes the oldest node (LRU position) and returns the node object so the Cache can identify which key to delete. + - `remove(node)`: Disconnects a node from its current position. This is used when an existing item is accessed or updated and needs to be moved to the front. + +### LruCache Class +- **`get(key)`**: + 1. Checks the dictionary for the key. + 2. If found, it uses `remove()` and `push_head()` to move the node to the front of the list, marking it as recently used. +- **`set(key, value)`**: + 1. **If key exists**: Updates the value and moves the node to the front. + 2. **If key is new**: + - Checks if the cache is at its `limit`. + - If full, it calls `pop_tail()` and deletes that node's key from the dictionary. + - Adds the new node to both the dictionary and the front of the list. + +## 3. Complexity & Trade-offs +- **Time Complexity**: Every operation (`get` and `set`) is **O(1)** because dictionary lookups and linked list pointer updates do not depend on the size of the cache. +- **Space Complexity**: I traded **Space for Time**. I used extra memory to store: + 1. A dictionary entry for every item. + 2. Two pointers (`next` and `previous`) for every node. +- **Trade-off Choice**: This extra memory usage is worth the benefit of having a cache that never slows down, even as the limit increases to thousands of items. + +## 4. Testing Suite +I expanded the test suite to include several critical edge cases: +- **Update Existing Key**: Verified that re-setting a key updates the value and refreshes its "recency" status. +- **Limit of One**: Confirmed that a cache with a limit of 1 correctly evicts the old item every time a new one is added. +- **Non-existent Keys**: Ensured that the cache gracefully returns `None` for missing keys. +- **Complex Values**: Verified that the cache can store non-primitive types like lists, proving it stores data by reference. \ No newline at end of file