#146 LRU Cache
frequently reported atAppleOracleSamsungByteDance / TikTokWalmartAdobe
the problem
In plain words
Design a cache with a fixed capacity. get(key) returns the value or -1. put(key, value) inserts or updates; if the cache is over capacity, evict the least recently used key. Both must run in O(1) average time.
in capacity 2: put(1,1), put(2,2), get(1), put(3,3), get(2), put(4,4), get(1), get(3), get(4)
out 1, -1, -1, 3, 4
put(3,3) evicts key 2 (key 1 was just used); put(4,4) evicts key 1.
- 1 ≤ capacity ≤ 3000
- 0 ≤ key ≤ 10⁴
- 0 ≤ value ≤ 10⁵
- At most 2·10⁵ calls to get and put
your turn
Try it before you peek
Read the examples, say the brute force out loud, then write code: here or on LeetCode ↗. Stuck for 20 minutes? Open one hint at a time.
Solve it in the browser
Design: implement every operation. Compiled by the real javac (Java 17) and judged right here — nothing is sent anywhere.
Solutions are hidden so you can try first. Even 20 minutes of real effort makes the answer stick far better than reading it.
Marking the problem solved, or opening every hint, also shows it.
the aha
Key insight
patternHashMap + doubly linked list
where this runsJava's LinkedHashMap is this exact structure: a HashMap with a doubly linked list through its entries, and in access order its Javadoc calls it well-suited to building LRU caches.
You need two things in O(1): find a key (HashMap) and move/remove an entry by recency (doubly linked list). The map stores nodes, so you can unlink any node instantly.
solutions
From brute force to optimal
Keep keys in usage order in a LinkedList; on each use, find and move the key.
- get/put: remove the key from the order list (search), append it to the end.
- When full, evict the first key.
order.remove(key) must search the list.
Map + order list.
class LRUCacheSlow { private final int capacity; private final Map<Integer, Integer> map = new HashMap<>(); private final LinkedList<Integer> order = new LinkedList<>(); // front = least recently used LRUCacheSlow(int capacity) { this.capacity = capacity; } public int get(int key) { if (!map.containsKey(key)) return -1; order.remove(Integer.valueOf(key)); // O(n): must search for the key order.addLast(key); return map.get(key); } public void put(int key, int value) { if (map.containsKey(key)) order.remove(Integer.valueOf(key)); else if (map.size() == capacity) map.remove(order.removeFirst()); // evict LRU map.put(key, value); order.addLast(key); }}Java's LinkedHashMap already is a HashMap + doubly linked list. Turn on access order and evict in removeEldestEntry.
- new LinkedHashMap<>(16, 0.75f, true).
- Override removeEldestEntry: size() > capacity.
- get → getOrDefault(key, -1); put → put.
Hash lookup + constant-time relinking inside LinkedHashMap.
One entry per key.
class LRUCacheBuiltIn { private final Map<Integer, Integer> map; LRUCacheBuiltIn(int capacity) { // accessOrder = true: every get/put moves the entry to the end (most recent) map = new LinkedHashMap<>(16, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) { return size() > capacity; // called after each insert: drop the oldest } }; } public int get(int key) { return map.getOrDefault(key, -1); } public void put(int key, int value) { map.put(key, value); }}Map key → node. Most recent node sits after the head sentinel; the LRU node sits before the tail sentinel.
- get: look up the node, unlink it, add it to the front.
- put existing: update value, move to front.
- put new: if full, unlink
tail.prevand remove its key from the map; then add the new node to the front.
Map operations are O(1) average; unlink/addFront change four references.
One node + one map entry per key.
class LRUCache { 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<>(); // key → node, O(1) lookup private final Node head = new Node(0, 0), tail = new Node(0, 0); // sentinels; head.next = most recent public 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); addFront(node); // touched → most recently used return node.value; } public void put(int key, int value) { Node node = map.get(key); if (node != null) { // update + refresh node.value = value; unlink(node); addFront(node); return; } if (map.size() == capacity) { // full → evict the node just before tail Node lru = tail.prev; unlink(lru); map.remove(lru.key); // why each node stores its key } node = new Node(key, value); map.put(key, node); addFront(node); } private void unlink(Node x) { x.prev.next = x.next; x.next.prev = x.prev; } private void addFront(Node x) { x.next = head.next; x.prev = head; head.next.prev = x; head.next = x; }}Dry run
in the room
How to say it in the interview
talk track
- Say the requirement out loud: "O(1) lookup → HashMap; O(1) reorder and evict → doubly linked list. Combine them."
- Explain the sentinels: no null checks in unlink/addFront.
- Explain why each node stores its key: on eviction you have the node and must delete it from the map.
- Mention LinkedHashMap as the production answer — but implement the manual version if asked.
traps
- Forgetting to move a key to the front on
get(a read counts as a use). - Forgetting that
puton an existing key also refreshes it — and must not evict anything. - Evicting from the map but not the list (or the reverse) — the two must always agree.
what they ask next
Follow-ups
LFU Cache (LC 460)?
Map key → node plus a map frequency → doubly linked list, and a minFreq variable. Still O(1) per operation.
Make it thread-safe?
Simplest: one lock around get/put. At scale: split the cache into shards, each with its own lock (lock striping).
before you submit
Edge cases to test
- Capacity 1.
- put on an existing key updates and refreshes it — no eviction.
- get on a missing key → -1, and nothing moves.
- get counts as a use: it can save a key from eviction.
practise next
Similar problems
- #707Design Linked ListMediumin Java OOP
- #641Design Circular DequeMediumin Queues
- #705Design HashSetEasyin Java OOP
- #155Min StackMediumin Stacks