Logo for ammarahmed.ca
Medium
Feb 18, 2026
#hashmap
#linked-list

146. LRU Cache

Design a data structure that follows the constraints of a Least Recently Used (LRU) cache.

Implement the LRUCache class:

  • LRUCache(int capacity) Initialize the LRU cache with positive size capacity.
    • int get(int key) Return the value of the key if the key exists, otherwise return -1.
      • void put(int key, int value) Update the value of the key if the key exists. Otherwise, add the key-value pair to the cache. If the number of keys exceeds the capacity from this operation, evict the least recently used key.

        The functions get and put must each run in O(1) average time complexity.

        Example 1:

        1Input 2["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] 3[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]] 4Output 5[null, null, null, 1, null, -1, null, -1, 3, 4] 6 7Explanation 8LRUCache lRUCache = new LRUCache(2); 9lRUCache.put(1, 1); // cache is {1=1} 10lRUCache.put(2, 2); // cache is {1=1, 2=2} 11lRUCache.get(1); // return 1 12lRUCache.put(3, 3); // LRU key was 2, evicts key 2, cache is {1=1, 3=3} 13lRUCache.get(2); // returns -1 (not found) 14lRUCache.put(4, 4); // LRU key was 1, evicts key 1, cache is {4=4, 3=3} 15lRUCache.get(1); // return -1 (not found) 16lRUCache.get(3); // return 3 17lRUCache.get(4); // return 4
        • 1 <= capacity <= 3000
          • 0 <= key <= 104
            • 0 <= value <= 105
              • At most 2 * 105 calls will be made to get and put.

                Constraints:

                Notes

                Intuition

                We need removal from anywhere and insertion at either end to all be O(1). Thus, we can use a hashmap with a doubly-linked list.

                Implementation

                In a nutshell, we store list nodes by key in a hashmap as well as keep track of our doubly-linked list with head and tail pointers.

                On get, we check if the node exists, if it does, we disconnect (remove) that node from the list and add it to the most-recently used (MRU) end which we can define as the tail. Both of these operations can be done in O(1) time as it is doubly-linked and the node is stored in a hashmap. If the node doesn't exist, simply return -1.

                On put, we check if the node exists, it it does, we update the value stored in the node. Then we disconnect (remove) that node from the list and add it to the MRU end. If it does not exist, we create a new node, add it to the hashmap, and then add the node to the MRU end. After this, we check if the length of our hashmap exceeds the capacity (need to evict), if so, we remove the node at the head of the list (LRU end), and delete the node from the hashmap using the key stored in the node.

                Edge-cases

                To remove a bunch of edge-cases, we can use sentinel (dummy) nodes at the head and tail so that we don't have to deal with many null pointer checks. This makes removals and insertions quite trivial.

                We should also ensure to store the key as well as the value in the list node so that we can easily delete the node from the hashmap on eviction.

                • Time: O(1): Since we use a hashmap to store nodes and a doubly-linked list, node lookup is O(1), removal is O(1), insertion is O(1)
                  • Space: O(n): where n is the capacity. At most we store the n nodes as they get evicted when capacity is reached.

                    Complexity

                    Solution

                    1from typing import List, Optional 2 3class Node: 4 def __init__( 5 self, 6 key: int, 7 val: int, 8 next: Optional['Node'] = None, 9 prev: Optional['Node'] = None, 10 ): 11 self.key = key 12 self.val = val 13 self.next = next 14 self.prev = prev 15 16 17class LRUCache: 18 def __init__(self, capacity: int): 19 self.capacity = capacity 20 self.nodes: dict[int, Node] = {} 21 self.head = Node(-1, -1) 22 self.tail = Node(-1, -1) 23 self.head.next = self.tail 24 self.tail.prev = self.head 25 26 def _remove(self, node: Node): 27 """Disconnects a node from the list (guranteed it exists)""" 28 prev = node.prev 29 nxt = node.next 30 if prev: 31 prev.next = nxt 32 if nxt: 33 nxt.prev = prev 34 node.prev = node.next = None 35 36 def _add_to_mru(self, node: Node): 37 """Adds a node to the MRU (tail) end""" 38 last = self.tail.prev 39 node.prev = last 40 node.next = self.tail 41 if last: 42 last.next = node 43 self.tail.prev = node 44 45 def _move_to_mru(self, node: Node): 46 """Moves an existing node to the MRU position""" 47 self._remove(node) 48 self._add_to_mru(node) 49 50 def _evict_lru(self): 51 """Evicts the node at the LRU position (head)""" 52 lru = self.head.next 53 if not lru or lru is self.tail: 54 return 55 self._remove(lru) 56 del self.nodes[lru.key] 57 58 59 def get(self, key: int) -> int: 60 if key in self.nodes: 61 node = self.nodes[key] 62 self._move_to_mru(node) 63 return node.val 64 return -1 65 66 67 def put(self, key: int, value: int) -> None: 68 if key in self.nodes: 69 node = self.nodes[key] 70 node.val = value 71 self._move_to_mru(node) 72 else: 73 node = Node(key, value) 74 self.nodes[key] = node 75 self._add_to_mru(node) 76 77 if len(self.nodes) > self.capacity: 78 self._evict_lru() 79 80 81if __name__ == "__main__": 82 cache = LRUCache(2) 83