Overview
A doubly linked list gives every node two pointers: next and prev. You can walk in both directions, and, most importantly, remove any node in O(1) when you hold a reference to it, because the node already knows its predecessor.
This property makes the doubly linked list the backbone of LRU caches, browser history, and deques. Paired with a hash map from key to node, it supports O(1) lookup, O(1) move-to-front, and O(1) eviction of the oldest item.
Every carriage is connected to the one in front and the one behind. To remove a carriage from the middle, you connect its neighbors to each other directly; you never need to walk from the engine to find who is in front.
When to use it
- You need O(1) removal of arbitrary nodes that you can locate quickly (usually through a hash map).
- You need to traverse both forward and backward.
- You maintain recency or order under frequent moves: LRU / LFU caches.
- You implement a deque or a text editor buffer.
Problem patterns it solves
Recognize it when: O(1) get and put with recency or frequency eviction.
- 146. LRU Cache
- 460. LFU Cache
- 432. All O`one Data Structure
Recognize it when: back / forward history, move cursor left and right.
- 1472. Design Browser History
- 2296. Design a Text Editor
Recognize it when: nodes with child pointers or multilevel lists.
- 430. Flatten a Multilevel Doubly Linked List
- 426. Convert BST to Sorted Doubly Linked List
Recognize it when: insert and delete at both ends in O(1).
- 641. Design Circular Deque
- 707. Design Linked List
Where it is used in real software
Redis approximates LRU eviction, and Java's LinkedHashMap with accessOrder = true is a hash map threaded through a doubly linked list, which is exactly an LRU cache.
Back and forward buttons move along a doubly linked sequence of visited pages.
The Linux kernel's list_head is a circular doubly linked list used for run queues and many other kernel lists.
Previous and next track navigation, and moving a song within a queue, map naturally to prev and next pointers.
Key terms
- prev / next
- Pointers to the previous and next nodes.
- Sentinel head and tail
- Dummy nodes at both ends so every real node always has non-null neighbors.
- Unlink
- node.prev.next = node.next; node.next.prev = node.prev.
- Move to front
- Unlink a node and insert it right after the head sentinel.
LRU cache operations
- 1Structure
A Map from key to node, and a doubly linked list with sentinel head (most recent side) and tail (least recent side).
- 2get(key)
Look up the node in the map. If found, move it to the front and return its value. O(1).
- 3put(key, value), existing key
Update the value and move the node to the front.
- 4put(key, value), new key
Create a node, insert it at the front, and add it to the map.
- 5Evict when over capacity
Remove the node before the tail sentinel (least recently used) from both the list and the map.
LRU cache with capacity 2
Front = most recent, back = least recent
| Operation | Result | List (front to back) | Map keys |
|---|---|---|---|
| put(1, A) | - | 1 | {1} |
| put(2, B) | - | 2, 1 | {1, 2} |
| get(1) | A | 1, 2 | {1, 2} |
| put(3, C) | evict 2 | 3, 1 | {1, 3} |
| get(2) | -1 | 3, 1 | {1, 3} |
| put(1, A2) | update | 1, 3 | {1, 3} |
NOWOperation: put(1, A) | Result: - | List (front to back): 1 | Map keys: {1}
Every operation touches a constant number of pointers and one hash map entry, so each is O(1).
Implementation
class Node { constructor(key, value) { this.key = key; this.value = value; this.prev = null; this.next = null; }} class LRUCache { constructor(capacity) { this.capacity = capacity; this.map = new Map(); this.head = new Node(); // most recent side this.tail = new Node(); // least recent side this.head.next = this.tail; this.tail.prev = this.head; } #unlink(node) { node.prev.next = node.next; node.next.prev = node.prev; } #addToFront(node) { node.next = this.head.next; node.prev = this.head; this.head.next.prev = node; this.head.next = node; } get(key) { const node = this.map.get(key); if (!node) return -1; this.#unlink(node); this.#addToFront(node); return node.value; } put(key, value) { if (this.map.has(key)) { const node = this.map.get(key); node.value = value; this.#unlink(node); this.#addToFront(node); return; } const node = new Node(key, value); this.map.set(key, node); this.#addToFront(node); if (this.map.size > this.capacity) { const lru = this.tail.prev; this.#unlink(lru); this.map.delete(lru.key); } }}Complexity and performance
Both neighbors are directly reachable.
With head and tail sentinels.
Needs a hash map for O(1) lookup.
Plus the value (and key for caches).
Trade-offs
Two pointers per node instead of one. For millions of small entries, this overhead is noticeable.
Every insert and delete updates four pointers; forgetting one silently corrupts the list.
Java's LinkedHashMap already implements the LRU structure; in interviews you are usually asked to build it yourself.
Variants and related techniques
One doubly linked list per frequency, plus a map from key to node and a pointer to the minimum frequency.
A single sentinel whose next is the head and prev is the tail; used in the Linux kernel.
Stores prev XOR next in one field to save memory; mostly a curiosity.
Common mistakes
- Updating pointers in the wrong order.
Fix: When inserting, set the new node's prev and next first, then update the neighbors.
- Forgetting to delete the evicted key from the map.
Fix: Store the key inside the node so you can remove it from the map during eviction.
- Null checks everywhere.
Fix: Use head and tail sentinels so neighbors always exist.
Interview questions
Why does an LRU cache need a doubly linked list rather than a singly linked one?
To move or remove an arbitrary node in O(1), you need its predecessor. A singly linked list would require an O(n) walk to find it.
Why not just use an array with timestamps?
Finding the least recently used item would take O(n) per eviction. The linked list keeps items in recency order so eviction is O(1).
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| 707. Design Linked List | Medium | Full implementation with sentinels. |
| 146. LRU Cache | Medium | Hash map plus doubly linked list. |
| 1472. Design Browser History | Medium | Bidirectional navigation. |
| 430. Flatten a Multilevel Doubly Linked List | Medium | Pointer splicing. |
| 460. LFU Cache | Hard | Frequency buckets. |