|
| 1 | +# Changes Made: LRU Cache Implementation |
| 2 | + |
| 3 | +## 1. Architectural Overview |
| 4 | +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**. |
| 5 | + |
| 6 | +- **The Dictionary (`self.lookup`)**: Provides constant time O(1) access to any node using its key. |
| 7 | +- **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. |
| 8 | + |
| 9 | +## 2. Component Breakdown |
| 10 | + |
| 11 | +### Node Class |
| 12 | +- **Key-Value Storage**: Unlike a standard linked list node, these nodes store both the `key` and the `value`. |
| 13 | +- **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. |
| 14 | + |
| 15 | +### LinkedList Class |
| 16 | +- **Pointer Management**: Manages `head` and `tail` pointers. |
| 17 | +- **Methods**: |
| 18 | + - `push_head(node)`: Places a node at the front (MRU position). |
| 19 | + - `pop_tail()`: Removes the oldest node (LRU position) and returns the node object so the Cache can identify which key to delete. |
| 20 | + - `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. |
| 21 | + |
| 22 | +### LruCache Class |
| 23 | +- **`get(key)`**: |
| 24 | + 1. Checks the dictionary for the key. |
| 25 | + 2. If found, it uses `remove()` and `push_head()` to move the node to the front of the list, marking it as recently used. |
| 26 | +- **`set(key, value)`**: |
| 27 | + 1. **If key exists**: Updates the value and moves the node to the front. |
| 28 | + 2. **If key is new**: |
| 29 | + - Checks if the cache is at its `limit`. |
| 30 | + - If full, it calls `pop_tail()` and deletes that node's key from the dictionary. |
| 31 | + - Adds the new node to both the dictionary and the front of the list. |
| 32 | + |
| 33 | +## 3. Complexity & Trade-offs |
| 34 | +- **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. |
| 35 | +- **Space Complexity**: I traded **Space for Time**. I used extra memory to store: |
| 36 | + 1. A dictionary entry for every item. |
| 37 | + 2. Two pointers (`next` and `previous`) for every node. |
| 38 | +- **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. |
| 39 | + |
| 40 | +## 4. Testing Suite |
| 41 | +I expanded the test suite to include several critical edge cases: |
| 42 | +- **Update Existing Key**: Verified that re-setting a key updates the value and refreshes its "recency" status. |
| 43 | +- **Limit of One**: Confirmed that a cache with a limit of 1 correctly evicts the old item every time a new one is added. |
| 44 | +- **Non-existent Keys**: Ensured that the cache gracefully returns `None` for missing keys. |
| 45 | +- **Complex Values**: Verified that the cache can store non-primitive types like lists, proving it stores data by reference. |
0 commit comments