Doubly Linked Lists & Circular Lists
📖 Concept
A doubly linked list has nodes with pointers to BOTH the next and previous nodes. This enables O(1) deletion of a given node and bidirectional traversal.
Structure:
null ← [10|←→] ⇄ [20|←→] ⇄ [30|←→] ⇄ [40] → null
↑ Head ↑ Tail
Advantages over singly linked lists:
- O(1) deletion of any node (if you have a reference to it)
- Bidirectional traversal — can go forward AND backward
- O(1) tail operations with tail pointer
- Used in: LRU Cache, browser history, text editors (undo/redo)
Circular Linked List: The last node points back to the first node (and in doubly, the first points back to last). Used in: round-robin scheduling, circular buffers, playlist loops.
Trade-offs:
- More memory per node (extra prev pointer)
- More complex insert/delete logic (update both prev and next)
- But O(1) operations at both ends make it ideal for deques and caches
🏠 Real-world analogy: A doubly linked list is like a two-way street — you can drive in either direction. A singly linked list is a one-way street. A circular list is a roundabout — keeps going around.
💻 Code Example
1// DOUBLY LINKED LIST NODE2class DLLNode {3 constructor(val) {4 this.val = val;5 this.prev = null;6 this.next = null;7 }8}910// DOUBLY LINKED LIST — Full implementation11class DoublyLinkedList {12 constructor() {13 // Sentinel nodes simplify edge cases14 this.head = new DLLNode(null); // Dummy head15 this.tail = new DLLNode(null); // Dummy tail16 this.head.next = this.tail;17 this.tail.prev = this.head;18 this.size = 0;19 }2021 // O(1) — Add to front22 addFirst(val) {23 const node = new DLLNode(val);24 node.next = this.head.next;25 node.prev = this.head;26 this.head.next.prev = node;27 this.head.next = node;28 this.size++;29 }3031 // O(1) — Add to back32 addLast(val) {33 const node = new DLLNode(val);34 node.prev = this.tail.prev;35 node.next = this.tail;36 this.tail.prev.next = node;37 this.tail.prev = node;38 this.size++;39 }4041 // O(1) — Remove specific node (given reference)42 removeNode(node) {43 node.prev.next = node.next;44 node.next.prev = node.prev;45 this.size--;46 return node.val;47 }4849 // O(1) — Remove from front50 removeFirst() {51 if (this.size === 0) return null;52 return this.removeNode(this.head.next);53 }5455 // O(1) — Remove from back56 removeLast() {57 if (this.size === 0) return null;58 return this.removeNode(this.tail.prev);59 }60}6162// LRU CACHE — Classic interview problem using DLL + Hash Map63class LRUCache {64 constructor(capacity) {65 this.capacity = capacity;66 this.map = new Map(); // key → DLLNode67 this.dll = new DoublyLinkedList();68 }6970 get(key) {71 if (!this.map.has(key)) return -1;72 const node = this.map.get(key);73 // Move to front (most recently used)74 this.dll.removeNode(node);75 this.dll.addFirst(node.val.value);76 // Update map reference77 this.map.set(key, this.dll.head.next);78 this.dll.head.next.val = { key, value: node.val.value };79 return node.val.value;80 }8182 put(key, value) {83 if (this.map.has(key)) {84 this.dll.removeNode(this.map.get(key));85 }86 this.dll.addFirst({ key, value });87 this.map.set(key, this.dll.head.next);88 if (this.map.size > this.capacity) {89 const last = this.dll.tail.prev;90 this.dll.removeNode(last);91 this.map.delete(last.val.key);92 }93 }94}9596// TypeScript DLL Node97class DLLNodeTS<T> {98 val: T;99 prev: DLLNodeTS<T> | null = null;100 next: DLLNodeTS<T> | null = null;101 constructor(val: T) { this.val = val; }102}
🏋️ Practice Exercise
Practice Problems:
- Implement a doubly linked list with addFirst, addLast, removeFirst, removeLast
- Build an LRU Cache using a doubly linked list + hash map
- Implement a circular linked list and traverse it
- Flatten a multilevel doubly linked list
- Design a browser history (back/forward) using a doubly linked list
- Implement a deque (double-ended queue) using a doubly linked list
- Reverse a doubly linked list
- Find pairs in a sorted doubly linked list that sum to a target
- Convert a binary search tree to a sorted doubly linked list
- Implement an undo/redo system using a doubly linked list
⚠️ Common Mistakes
Forgetting to update BOTH prev and next pointers during insert/delete — leads to broken links
Not using sentinel/dummy nodes — makes head/tail edge cases much more complex
Memory leaks — not nullifying prev/next when removing nodes in languages with manual memory
Confusing node reference with node value — especially in LRU cache implementations
Not maintaining the size counter — off-by-one errors when checking empty/full
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Doubly Linked Lists & Circular Lists