Linked lists come in several structural varieties. The two most common are the Singly Linked List (unidirectional) and the Doubly Linked List (bidirectional).
Singly Linked List (SLL)
In a Singly Linked List, each node points only forward to its successor (next).
Head
|
v
[ 10 | * ] ---> [ 20 | * ] ---> [ 30 | null ]
Advantages:
- Smaller Memory Footprint: Only one reference pointer (
next) per node. - Simpler implementation with fewer pointer updates during insertion and deletion.
Limitations:
- One-way traversal: You cannot step backward.
- Deletion is awkward: Deleting a given node
Nrequires traversing fromheadto find the node immediately precedingNinO(n)time.
Doubly Linked List (DLL)
In a Doubly Linked List, each node maintains two reference pointers:
next: Points to the successor node.prev: Points to the predecessor node.
Head Tail
| |
v v
null <-- [ * | 10 | * ] <===> [ * | 20 | * ] <===> [ * | 30 | * ] --> null
In Kotlin:
class DoublyNode<T>(
var data: T,
var prev: DoublyNode<T>? = null,
var next: DoublyNode<T>? = null
)
Deletion Comparison: The Decisive Advantage of DLL
Consider deleting a node target when you already hold a direct reference to it:
In Singly Linked List: O(n)
You cannot modify prev.next because you don't know who prev is! You must scan from head:
// Singly Linked List: O(n) traversal needed to find predecessor
fun deleteNode(head: Node<T>, target: Node<T>) {
var curr = head
while (curr.next != null && curr.next != target) {
curr = curr.next!!
}
curr.next = target.next // Bypass target
}
In Doubly Linked List: O(1)
Because the node knows its predecessor (node.prev), removal is instantaneous:
// Doubly Linked List: O(1) constant time removal!
fun deleteNode(target: DoublyNode<T>) {
target.prev?.next = target.next
target.next?.prev = target.prev
target.prev = null
target.next = null
}
This O(1) node self-deletion is why LRU Caches and Android Navigation Stacks rely on Doubly Linked Lists.
Comprehensive Comparison Matrix
| Feature | Singly Linked List | Doubly Linked List |
|---|---|---|
| Pointers per node | 1 (next) | 2 (prev, next) |
| Memory per node | Lower (~24 bytes) | Higher (~32–40 bytes) |
| Traversal Direction | Forward only | Forward and Backward |
| Insert / Delete at Head | O(1) | O(1) |
| Insert / Delete at Tail | O(1) (with tail pointer) | O(1) |
| Delete arbitrary node (given node pointer) | O(n) (must find predecessor) | O(1) |
| Reverse traversal | Requires recursion/stack (O(n)) | O(1) directly via prev |
Summary
- Choose Singly Linked Lists when memory is constrained and traversal is strictly forward (e.g., hash collision buckets).
- Choose Doubly Linked Lists when bidirectional navigation or
O(1)node removal is needed (e.g., LRU cache, text editor cursors).