146. LRU Cache
## description
## 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