Doubly Linked List
A doubly linked list gives every node two pointers instead of one — one pointing forward to the next node, and one pointing backward to the previous node. That extra backward link is what lets you walk the list in either direction.
Because both a head and tail pointer are kept, you get O(1) access at either end. The chain of "next" pointers reads the list forward, while the chain of "previous" pointers reads it backward.
The tradeoff is straightforward: you pay for an extra pointer per node in memory, but in exchange you get backward traversal and fast operations at both ends, which a singly linked list can't offer as cheaply.
Key Property: Each node is represented as [prev|data|next], showing the bidirectional links between nodes.
Basic Operations
| Operation | Complexity | Description |
|---|---|---|
| Insertion at Head | O(1) | Add new node at beginning, update head and adjacent node's pointers |
| Insertion at Tail | O(1) | Add new node at end using tail pointer |
| Insertion at Position | O(n) | Traverse to position and insert with pointer updates |
| Deletion at Head | O(1) | Remove first node and update head pointer |
| Deletion at Tail | O(1) | Remove last node using tail pointer |
| Deletion by Value | O(n) | Traverse to find node and update adjacent pointers |
| Forward Traversal | O(n) | Traverse from head to tail using next pointers |
| Backward Traversal | O(n) | Traverse from tail to head using prev pointers |
Implementation
class DoublyNode { constructor(data) { this.data = data; this.prev = null; this.next = null; }}class DoublyLinkedList { constructor() { this.head = null; this.tail = null; this.size = 0; } isEmpty() { return this.head === null; } // Insert at head insertFirst(data) { const newNode = new DoublyNode(data); if (this.isEmpty()) { this.head = newNode; this.tail = newNode; } else { newNode.next = this.head; this.head.prev = newNode; this.head = newNode; } this.size++; }Insertion Process
- 1. Create new node with data, prev, and next pointers
- 2. For head insertion: Set new node's next to current head
- 3. Update current head's prev to new node
- 4. Move head pointer to new node
- 5. For empty list, set both head and tail to new node
- 6. For tail insertion: Similar steps but working from tail
Deletion Process
- 1. Check if list is empty
- 2. For head deletion: Store head reference, move head to head.next
- 3. Set new head's prev to null (if exists)
- 4. For tail deletion: Similar steps working from tail
- 5. For middle deletion: Find node, update adjacent nodes' pointers
- 6. Handle special cases (single node removal)
Operation Visualization
| Operation | List State |
|---|---|
| Initialization | head → null ← tail |
| insertFirst(10) | head → [null|10|•] ← tail |
| insertFirst(20) | head → [null|20|•] ↔ [•|10|•] ← tail |
| insertLast(30) | head → [null|20|•] ↔ [•|10|•] ↔ [•|30|null] ← tail |
| deleteFirst() | head → [null|10|•] ↔ [•|30|null] ← tail |
| deleteLast() | head → [null|10|null] ← tail |
Comparison with Singly Linked List
| Feature | Singly Linked List | Doubly Linked List |
|---|---|---|
| Traversal Direction | Forward only | Both directions |
| Memory Overhead | Lower (1 pointer/node) | Higher (2 pointers/node) |
| Insert/Delete at Head | O(1) | O(1) |
| Insert/Delete at Tail | O(n) (or O(1) with tail pointer) | O(1) |
| Delete Current Node | Requires previous node | Direct access via prev pointer |
| Implementation Complexity | Simpler | More complex |
Pros and Cons
Advantages
- Bidirectional traversal capability
- O(1) operations at both ends
- Easier node removal (no need to track previous node)
- Better for certain algorithms (e.g., LRU cache)
Limitations
- Extra memory for prev pointers
- More pointer operations (slightly complex implementation)
- Slightly slower operations due to extra pointer updates
Applications
- Browser forward/backward navigation
- Undo/Redo functionality in software
- LRU (Least Recently Used) cache implementation
- Navigation systems with bidirectional movement
- Music/video playlists with forward/backward controls
- Text editors with cursor movement in both directions
When to Choose: Prefer doubly linked lists when you need bidirectional traversal, frequent operations at both ends, or when the ability to delete arbitrary nodes without traversal is valuable.