-
-
Notifications
You must be signed in to change notification settings - Fork 48
West Midlands | 26 March SDC | Iswat Bello | Sprint 2 | Implement an LRU cache in python #193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Iswanna
wants to merge
10
commits into
CodeYourFuture:main
Choose a base branch
from
Iswanna:implement-an-LRU-cache-in-python
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
3737e45
feat: define Node class with key-value storage for LRU Cache
Iswanna a19e9bf
feat: add LinkedList class to manage cache usage order
Iswanna 51c5c71
feat: implement push_head method for LinkedList
Iswanna 01c34ee
feat: implement pop_tail method for LinkedList
Iswanna abaca89
feat: implement remove method for LinkedList
Iswanna 7621805
feat: implement LruCache class and initialization logic
Iswanna e2d5a85
feat: implement get method for LruCache
Iswanna db31358
feat: implement set method with LRU eviction logic
Iswanna becdd11
test: add edge case and complex value tests for LruCache
Iswanna f1260f0
docs: add detailed implementation notes for LRU Cache
Iswanna File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| 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 | ||
|
|
||
|
|
||
| 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 | ||
|
|
||
| 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 | ||
|
|
||
| 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 | ||
|
|
||
| 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 | ||
|
|
||
|
|
||
| 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() | ||
|
|
||
| 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 | ||
|
|
||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To practice code reuse, you could import the linked list you implemented in the other exercise and store the key-value pairs as a tuple in each node.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the suggestion.
By storing the key and value together as a tuple inside the node, I could use the Linked List I already built without having to write a new one for this file. Thanks for the suggestion.