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.
How It Lives in Memory
Like any linked list, a doubly linked list's nodes are scattered across memory rather than sitting in one contiguous block. The chain only exists because of the pointers each node stores, not because of where the nodes physically live.
The difference here is that every node carries two of those pointers instead of one, so the per-node overhead is doubled. In exchange, any node you already hold a reference to can be removed in O(1), because it knows both of its neighbours and can splice itself out without a traversal to find the one behind it.
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 |
How Does It Work?
Each node below is drawn as its three parts: a prev cell, the data, and a next cell. The grey address under each box is where that node lives in memory, and the pointer cells hold nothing but addresses — prev stores the address of the node behind, next the address of the node ahead. At the two ends there is nothing to point at, so those cells read null. Every pair of neighbours is therefore joined by two links, one running forward along the top and one running back along the bottom, and keeping both correct is the entire job of every operation on this list.
Read across the middle node below: its prev says 0x1A and its next says 0x3C, which are exactly the addresses printed under its two neighbours. The addresses here are made up and kept short so they fit; real ones are far longer but behave identically.
Insertion Process
Inserting at the head means writing four pointers in total: the new node's prev (null) and next (the old head's address), the old head's prev (the new node's address), and the head pointer itself. Watch A's prev cell below change from null to 0x4D. No traversal is involved, so it runs in O(1).
↓ insert X at head ↓
- 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
To remove a node, its two neighbours are pointed at each other: X's next is overwritten with the address in A's next (0x2F), and B's prev is overwritten with the address in A's prev (0x4D). Crucially, A already holds both of those addresses in its own cells, so no walk from the head is needed to find the node in front of it — which is exactly what a singly linked list would have to do.
↓ delete A ↓
- 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 Walkthrough
A full sequence on an empty list, one operation at a time:
Initialization — An empty list is just two null pointers.
insertFirst(10) — The first node is both head and tail, and both of its pointer cells are null.
insertFirst(20) — The new node's next points at 10, and 10's prev points back at it — one link written in each direction.
insertLast(30) — Because tail is tracked, the end is reached without walking the list, so this is O(1) rather than O(n).
deleteFirst() — head moves to 10 and 10's prev is set to null. The detached node is now unreachable.
deleteLast() — tail moves back to 10 using 30's prev pointer — the step a singly linked list cannot take without traversing.
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.