Linked Lists
Overview
A linked list is a sequence of nodes, each containing data and one or more pointers to neighbouring nodes. Lists provide dynamic sizing (no pre-allocation needed) and constant-time insertion/deletion at known positions, at the cost of random access.
Singly Linked Lists
Each node has a value field and a next pointer. The list is accessed via a head pointer (and optionally a tail pointer for constant-time append).
Operations
| Operation | Without tail | With tail |
|---|---|---|
INSERT-HEAD | ||
INSERT-TAIL | ||
DELETE-HEAD | ||
DELETE(x) given node | (need predecessor) | |
DELETE(x) given predecessor | ||
SEARCH(k) |
Insertion at head: create a new node, point its next to the current head, update head to the new node. This is .
Deletion of a node in a singly linked list requires finding the predecessor of (to update its next pointer), which takes in the worst case via a linear scan from head. If the predecessor is already known (e.g. when iterating), deletion is .
Doubly Linked Lists
Each node has prev, value, and next pointers. The list maintains head and optionally tail.
The key advantage: given a pointer to a node , deletion is — we can reach its predecessor via without a scan.
DELETE(x): # x is a node in a doubly linked list
if x.prev != NIL:
x.prev.next = x.next
else:
L.head = x.next # x was head
if x.next != NIL:
x.next.prev = x.prev
Insertion at head or after a known node is also :
INSERT-HEAD(L, k):
x = new Node(prev=NIL, key=k, next=L.head)
if L.head != NIL: L.head.prev = x
L.head = x
Circular Variants
A circular linked list connects the last node back to the first (and optionally the first’s prev to the last in the doubly linked case). A sentinel node can be used as both head and tail, simplifying boundary checks.
Circular singly linked lists with tail pointer enable insertion at both ends and deletion at the front.
Comparison with Arrays
| Aspect | Array | Linked List |
|---|---|---|
| Random access (-th element) | ||
| Insert/delete at head | (shift elements) | |
| Insert/delete at tail | amortised or | with tail pointer |
| Insert/delete in middle | (shift) | if position known, to find |
| Search | linear, if sorted | |
| Space overhead | None beyond array itself | 1–2 pointers per element |
| Cache locality | Excellent (contiguous memory) | Poor (scattered allocations) |
Space Overhead
| Type | Pointers per node | Notes |
|---|---|---|
| Singly linked | 1 (next) | Forward traversal only |
| Doubly linked | 2 (next, prev) | Bidirectional; enables deletion at known node |
| Circular | Same as linear variant | Extra pointer logic but no extra fields |
Usage in Doug Lea’s Malloc
The classic malloc implementation uses a doubly linked list to track free memory chunks in the heap. Each chunk is preceded by a list node containing the chunk size and free/busy status. Free chunks are linked together; when a chunk is freed, it is coalesced with adjacent free chunks. This is a real-world application of doubly linked lists at massive scale.
Summary
| Property | Singly Linked | Doubly Linked |
|---|---|---|
| Head insert | ||
| Tail insert | (or with tail) | with tail |
| Delete given node | (need predecessor) | |
| Search | ||
| Space per node | 1 pointer | 2 pointers |
| Traversal | Forward only | Bidirectional |