Backlog·queued
LRU Cache
DifficultyMedium
PatternIn place reversal LinkedList
TrackDSA
tl;dr
Design a chache system
full write-upThe helper
The helper
The method
The method
LRU Cache
Problem
Design a data structure that works as a Least Recently Used (LRU) cache.
Implement the LRUCache class:
LRUCache(capacity): Create the cache with a positive sizecapacity.get(key): Return the value of the key if it exists. Otherwise, return-1.put(key, value): Update the value if the key exists. Otherwise, add the key-value pair. If the number of keys becomes bigger thancapacity, remove the least recently used key.
Both get and put must run in O(1) average time.
Examples
Example 1
Input
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
Output
[null, null, null, 1, null, -1, null, -1, 3, 4]
put(1, 1): cache is{1=1}.put(2, 2): cache is{1=1, 2=2}.get(1): returns1. Now key2is the least recently used.put(3, 3): the cache is full, so key2is removed. Cache is{1=1, 3=3}.get(2): returns-1, because key2was removed.put(4, 4): key1is the least recently used, so it is removed. Cache is{3=3, 4=4}.get(1): returns-1.get(3): returns3.get(4): returns4.
Constraints
1 <= capacity <= 30000 <= key <= 10^40 <= value <= 10^5- At most
2 * 10^5calls are made togetandput.
Main Idea
We need two things to be fast:
- Find a key quickly.
- Move a key to the "most recent" position, and remove the oldest key, quickly.
One data structure cannot do both. So we use two together:
- Hash Map: It maps each
keyto its node. This finds any key inO(1). - Doubly Linked List: It keeps the keys in order of use. This lets us remove a node and add a node in
O(1), because each node knows both its neighbors.
How the list is arranged
head <-> [most recent] <-> ... <-> [least recent] <-> tail
- Nodes near
headwere used recently. - The node just before
tailis the least recently used. headandtailare fake nodes. They never hold data. They help us avoid special cases for an empty list or the first and last node.
Rules
- Using a key (
getorput): Remove its node from the list, and add it right afterhead. It is now the most recent. - Adding a new key beyond capacity: Remove the node right before
tail. Also delete its key from the map.
Why does the node store its
key? When we evict the last node, we only have the node. We need its key to delete it from the map.
Solution: Hash Map + Doubly Linked List
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.map = new Map();
// fake nodes at both ends
this.head = {};
this.tail = {};
this.head.next = this.tail;
this.tail.prev = this.head;
}
get(key) {
// key is not in the cache
if (!this.map.has(key)) return -1;
let node = this.map.get(key);
// move the node to the front (most recent)
this.remove(node);
this.add(node);
return node.value;
}
put(key, value) {
// key already exists: update it and move it to the front
if (this.map.has(key)) {
let node = this.map.get(key);
node.value = value;
this.remove(node);
this.add(node);
return;
}
// new key: create a node and add it to the front
let node = { key, value };
this.map.set(key, node);
this.add(node);
// too many keys: remove the least recently used one
if (this.map.size > this.capacity) {
let lru = this.tail.prev;
this.remove(lru);
this.map.delete(lru.key);
}
}
// take a node out of the list
remove(node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
// put a node right after head
add(node) {
node.next = this.head.next;
node.prev = this.head;
this.head.next.prev = node;
this.head.next = node;
}
}
How the Code Works
The constructor
capacityis the maximum number of keys.mapfinds a node by its key.headandtailare fake nodes. We link them to each other, so the empty list ishead <-> tail.
The helper remove(node)
- It connects the node's left neighbor directly to its right neighbor.
- The node is now out of the list. It takes
O(1), because we do not search.
The helper add(node)
- It puts the node right after
head. - The steps are:
- Point the node's
nextto the old first node. - Point the node's
prevtohead. - Point the old first node's
prevback to the node. - Point
head.nextto the node.
- Point the node's
- The order matters. We use
this.head.nextbefore we change it.
The method get(key)
- If the key is not in the map, return
-1. - Otherwise, move the node to the front, because it was just used, and return its value.
The method put(key, value)
- Key exists: Update the value and move the node to the front.
- Key is new:
- Create the node, save it in the map, and add it to the front.
- If the size is now bigger than
capacity, takethis.tail.prev. It is the least recently used node. Remove it from the list and from the map.
Dry Run
Let us use capacity = 2. We write the list from most recent to least recent.
| Operation | Action | List (recent -> old) |
|---|---|---|
put(1, 1) | Add key 1 | [1] |
put(2, 2) | Add key 2 | [2, 1] |
get(1) | Move key 1 to the front. Return 1 | [1, 2] |
put(3, 3) | Add key 3. Size is 3, so remove tail.prev (key 2) | [3, 1] |
get(2) | Not in the map. Return -1 | [3, 1] |
put(4, 4) | Add key 4. Size is 3, so remove key 1 | [4, 3] |
get(1) | Not in the map. Return -1 | [4, 3] |
get(3) | Move key 3 to the front. Return 3 | [3, 4] |
get(4) | Move key 4 to the front. Return 4 | [4, 3] |
The outputs are 1, -1, -1, 3, 4, the same as the example.
Complexity
- Time Complexity:
O(1)for bothgetandput- Map operations (
has,get,set,delete) takeO(1)on average. removeandaddonly change a few pointers.- We never loop through the list.
- Map operations (
- Space Complexity:
O(capacity)- The map and the list each hold at most
capacitynodes (plus one temporary extra node duringput).
- The map and the list each hold at most
Shorter Version (Optional)
A JavaScript Map remembers the order in which keys were added. So we can use it alone. To make a key "most recent", we delete it and add it again. The first key in the map is the least recently used.
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.map = new Map();
}
get(key) {
if (!this.map.has(key)) return -1;
let value = this.map.get(key);
// delete and set again to make it the most recent
this.map.delete(key);
this.map.set(key, value);
return value;
}
put(key, value) {
if (this.map.has(key)) {
this.map.delete(key);
}
this.map.set(key, value);
// remove the oldest key (the first one in the map)
if (this.map.size > this.capacity) {
let oldest = this.map.keys().next().value;
this.map.delete(oldest);
}
}
}
- It is short and works well in practice, and its operations take
O(1)on average. - In interviews, the Map + Doubly Linked List version is the expected answer. It shows that you know how the data structure works inside.
Comparison
| Approach | Time Complexity (get / put) | Space Complexity |
|---|---|---|
| Hash Map + Doubly Linked List | O(1) | O(capacity) |
Ordered Map only | O(1) average | O(capacity) |
Summary
- An LRU cache needs fast lookup and fast reordering. So we combine two structures.
- The hash map finds a node by key in
O(1). - The doubly linked list keeps the use order. The front is the most recent, and the node before
tailis the least recent. - Fake
headandtailnodes remove special cases. - Every
getorputof an existing key moves its node to the front. - When the size goes over
capacity, we removetail.prevfrom both the list and the map. The node stores itskeyfor this reason. - Both
getandputrun inO(1)time, and the cache usesO(capacity)space.