Back to Catalog
Data Structures
Sequential Access
Doubly Linked Lists
Bidirectional node chain — O(1) insert/delete at both ends, backward traversal included.
Intent & Description
Real-world Use Case
LRU Cache — hash map gives O(1) lookup, doubly linked list gives O(1) eviction of the least recently used node. Browser history — back and forward navigation. Text editor cursor movement — character nodes with prev/next for O(1) insert at cursor position.
Source
📌 TL;DR
Doubly Linked List = O(1) at both ends plus O(1) node removal by pointer. The structure inside every LRU cache. Extra prev pointer doubles memory vs singly linked — worth it when you need bidirectional ops.
Advantages
- O(1) insert and delete at both ends — not just the head
- O(1) node removal given a direct node pointer (no traversal needed)
- Bidirectional traversal — iterate forwards and backwards
- Sentinel nodes eliminate null-check complexity in implementation
Disadvantages
- Double the pointer overhead — 16 bytes per node (next + prev on 64-bit) vs 8 bytes for singly linked
- More complex to implement correctly — four pointer updates per insertion
- Still O(n) for index-based access and search by value
- Scattered heap allocation — worse cache performance than arrays
Implementation Example
class DLNode {
constructor(key, value) {
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
}
// Doubly Linked List with sentinel head/tail (cleaner edge-case handling)
class DoublyLinkedList {
constructor() {
// Dummy sentinels — never removed, simplify all insertions/deletions
this.head = new DLNode(null, null); // Sentinel head
this.tail = new DLNode(null, null); // Sentinel tail
this.head.next = this.tail;
this.tail.prev = this.head;
this.size = 0;
}
// O(1) — Insert node right after head sentinel (most-recently-used position)
insertFront(node) {
node.next = this.head.next;
node.prev = this.head;
this.head.next.prev = node;
this.head.next = node;
this.size++;
}
// O(1) — Remove any node given its pointer (no traversal)
remove(node) {
node.prev.next = node.next;
node.next.prev = node.prev;
node.prev = null;
node.next = null;
this.size--;
}
// O(1) — Remove the node just before tail sentinel (least-recently-used)
removeLast() {
if (this.tail.prev === this.head) return null; // Empty
const node = this.tail.prev;
this.remove(node);
return node;
}
}
// ─── LRU Cache built on DLL + HashMap ─────────────────────────
class LRUCache {
#capacity;
#map = new Map(); // key → node (O(1) lookup)
#list = new DoublyLinkedList(); // ordered by recency
constructor(capacity) { this.#capacity = capacity; }
// O(1) — Get and mark as most recently used
get(key) {
if (!this.#map.has(key)) return -1;
const node = this.#map.get(key);
this.#list.remove(node); // Splice out
this.#list.insertFront(node); // Move to front (most recent)
return node.value;
}
// O(1) — Put and evict LRU if over capacity
put(key, value) {
if (this.#map.has(key)) {
this.#list.remove(this.#map.get(key));
} else if (this.#list.size === this.#capacity) {
const lru = this.#list.removeLast(); // Evict least recently used
this.#map.delete(lru.key);
}
const node = new DLNode(key, value);
this.#list.insertFront(node);
this.#map.set(key, node);
}
}
const cache = new LRUCache(3);
cache.put('a', 1); cache.put('b', 2); cache.put('c', 3);
cache.get('a'); // Moves 'a' to front
cache.put('d', 4); // Evicts 'b' (LRU) — cache is now {a,c,d}
console.log(cache.get('b')); // -1 (evicted)
console.log(cache.get('a')); // 1