Topics › Stacks and Queues › Implementation Problems
LRU Cache
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
Design a cache with a fixed capacity that supports two operations:
get(key): return the value stored forkey, or-1if the key is not present.put(key, value): insert the key, or update its value if it is already present. If inserting a new key would exceed the capacity, first evict the least recently used key.
Both a get that finds the key and any put count as a “use” of that key. Both operations must run in O(1) average time.
Constraints: 1 ≤ capacity ≤ 3000, keys and values are non-negative integers, at most 2 × 10^5 calls in total.
All implementations in the Java file share one small interface, so the same test script can drive each of them:
interface Cache {
int get(int key);
void put(int key, int value);
}
Example
capacity = 2
put(1, 1) cache order (most recent first): 1
put(2, 2) 2, 1
get(1) -> 1 1, 2 (1 becomes most recent)
put(3, 3) 3, 1 (full: evict 2, the least recent)
get(2) -> -1
put(4, 4) 4, 3 (full: evict 1)
get(1) -> -1
get(3) -> 3 3, 4
get(4) -> 4 4, 3
What the data structure must do. Look up a key by value in O(1) (a hash map can do that). Keep the keys in recency order, move any key to the “most recent” end in O(1), and remove the key at the “least recent” end in O(1) (a linked list can do that, if we can reach a node without searching for it). Neither structure is enough alone, and combining them is the whole solution.
Approach 1: Brute force, a list in recency order
Intuition. Keep {key, value} entries in a list ordered from least to most recently used. Every operation scans for the key. A hit is removed and re-appended at the end, which makes it the most recent. When a new key arrives and the list is full, the front entry is the least recent one, so remove it.
Algorithm.
get: scan for the key; if found, move its entry to the end and return the value; else return-1.put: remove the key’s entry if it exists; if the list is full, remove the first entry; append{key, value}.
static class BruteLRU implements Cache {
private final int capacity;
private final List<int[]> entries = new ArrayList<>(); // {key, value}, least recently used first
BruteLRU(int capacity) { this.capacity = capacity; }
public int get(int key) {
for (int i = 0; i < entries.size(); i++) {
if (entries.get(i)[0] == key) {
int[] e = entries.remove(i);
entries.add(e); // now the most recently used
return e[1];
}
}
return -1;
}
public void put(int key, int value) {
for (int i = 0; i < entries.size(); i++) {
if (entries.get(i)[0] == key) {
entries.remove(i);
break;
}
}
if (entries.size() == capacity) entries.remove(0); // evict the least recently used
entries.add(new int[]{key, value});
}
}
Complexity. O(capacity) per operation: the scan, and ArrayList.remove(i) shifts the elements after i. Space O(capacity). With 2 × 10^5 calls and capacity 3000 that is up to 6 × 10^8 steps.
Note that entries.remove(i) is called with an int, which selects remove(int index). On a List<Integer> this overload trap would matter; here the elements are int[], so there is no ambiguity.
Approach 2: Optimal, hash map plus doubly linked list
Intuition. Keep the entries in a doubly linked list in recency order: the node right after head is the most recent, the node right before tail is the least recent. A doubly linked list can unlink any node in O(1) if we already hold a reference to it, because the node knows both of its neighbours. The hash map provides exactly that: it maps each key to its node.
Two details make the code short and safe:
- Sentinel nodes.
headandtailare dummy nodes that are always present. Every real node therefore has a non-nullprevandnext, andunlinkandinsertFrontneed no special cases for an empty list or the first and last positions. - The node stores its key. When we evict
tail.prev, we must also delete it from the map, and the map is indexed by key. Withoutnode.keywe would not know which map entry to remove.
Algorithm.
get(key): look up the node; if missing return-1. Otherwise unlink it, insert it right afterhead, and return its value.put(key, value):- If the key exists: update the value, move the node to the front, done. No eviction is needed because the size does not change.
- Otherwise, if the map is full: unlink
tail.prevand remove its key from the map. - Create a node, put it in the map, insert it at the front.
static class LRUCache implements Cache {
private static class Node {
int key, value;
Node prev, next;
Node(int key, int value) { this.key = key; this.value = value; }
}
private final int capacity;
private final Map<Integer, Node> map = new HashMap<>();
private final Node head = new Node(0, 0), tail = new Node(0, 0); // sentinels; head.next is the most recent
LRUCache(int capacity) {
this.capacity = capacity;
head.next = tail;
tail.prev = head;
}
public int get(int key) {
Node node = map.get(key);
if (node == null) return -1;
unlink(node);
insertFront(node);
return node.value;
}
public void put(int key, int value) {
Node node = map.get(key);
if (node != null) {
node.value = value;
unlink(node);
insertFront(node);
return;
}
if (map.size() == capacity) {
Node lru = tail.prev;
unlink(lru);
map.remove(lru.key);
}
node = new Node(key, value);
map.put(key, node);
insertFront(node);
}
private void unlink(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void insertFront(Node node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
}
Dry run of the first five operations of the example (list shown from head to tail):
| operation | list after | map keys | returned |
|---|---|---|---|
| put(1, 1) | 1 | {1} | |
| put(2, 2) | 2, 1 | {1, 2} | |
| get(1) | 1, 2 | {1, 2} | 1 |
| put(3, 3) | 3, 1 | {1, 3} | evicts 2 |
| get(2) | 3, 1 | {1, 3} | -1 |
Complexity. O(1) average per operation: one hash map access and a constant number of pointer updates. Space O(capacity) for the map and the nodes.
Approach 3: Library shortcut, LinkedHashMap
Intuition. Java’s LinkedHashMap is precisely “hash map plus doubly linked list”. Constructed with accessOrder = true, it moves an entry to the end of its internal list on every get and put, so iteration order is least recent first. It also calls removeEldestEntry after each insertion; overriding it to return size() > capacity makes the map evict its least recently used entry automatically.
static class LinkedHashMapLRU implements Cache {
private final LinkedHashMap<Integer, Integer> map;
LinkedHashMapLRU(int capacity) {
map = new LinkedHashMap<>(16, 0.75f, true) { // true = order by access, not insertion
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
};
}
public int get(int key) {
return map.getOrDefault(key, -1);
}
public void put(int key, int value) {
map.put(key, value);
}
}
Complexity. O(1) average per operation, O(capacity) space.
This is worth knowing for production code. In an interview, expect to be asked to build Approach 2 yourself, because that is what the question is testing. Also note that getOrDefault counts as an access in an access-ordered LinkedHashMap, just like get.
Edge cases and pitfalls
- Updating an existing key.
puton a present key must change the value and refresh its recency, without evicting anything. A common bug is to checksize == capacityfirst and evict a key even though the cache will not grow. getrefreshes recency. In the testput 1, put 2, get 1, put 3, key2is evicted, not key1.- Capacity 1. Every new key evicts the previous one. The sentinel version needs no special case for this.
- Removing from the map on eviction. Unlinking the node but leaving it in the map makes later
getcalls return evicted values. - Order of operations in
insertFront. Set the new node’snextandprevbefore changinghead.next; otherwise the old first node is lost. - Values of
-1. The interface uses-1for “absent”, so it assumes stored values are non-negative, as the problem guarantees.
Related problems
- LFU Cache evicts by frequency first and recency second, using one of these lists per frequency.
- Implement Min Stack is another design problem where an extra structure gives
O(1)queries. - Introduction to Doubly LL covers the
prevandnextpointer updates used here. - Implement Queue using Stack is a smaller warm-up in combining structures to meet a complexity target.