Overview
A linked list is a dynamic sequence built from nodes. Each node stores a value and a pointer to the next node (singly linked list, SLL). The list container maintains a pointer to the head (first node), and optionally a tail (last node) and a size counter. The shape naturally supports O(1) insertion and deletion once the predecessor is known, and O(n) traversal. Unlike arrays, linked lists avoid contiguous memory and shifting costs, but pay overhead in pointers, cache misses, and indirection.
Structure Definition
Node and container
Node:
value
next // Node or NIL
List:
head : Node or NIL // plain: first real node or NIL
tail : Node or NIL // optional: last real node (if maintained)
size : integer // optional: number of real nodes
Invariants
-
head == NIL ⇔ tail == NIL ⇔ size == 0(whensizeis tracked). -
If
tail != NILthentail.next == NIL. -
Following
nextfromheadends atNIL(no cycles) unless explicitly using a circular variant (see Circular Linked List).
Tip
Sentinel head. Introduce a dummy head whose
nextpoints to the first real node. Many edge cases (insert/delete at front) become ordinary middle cases becauseheadis neverNIL. Store the dummy in theListobject and keeptailpointing to the last real node.
Core Operations
Below, “SLL” means a singly linked list without cycles. Operations are O(1) when the predecessor is available; otherwise, locating it costs O(n).
Insert at front (push-front)
function PUSH_FRONT(L, x):
n = new Node(x)
n.next = L.head
L.head = n
if L.tail == NIL: L.tail = n
L.size = L.size + 1Insert at back (push-back) using tail
function PUSH_BACK(L, x):
n = new Node(x)
n.next = NIL
if L.tail != NIL:
L.tail.next = n
L.tail = n
else:
L.head = n
L.tail = n
L.size = L.size + 1Tip
Without a
tail, appending requires a full traversal to the last node (O(n)). Maintaintailfor queues.
Insert after node p
function INSERT_AFTER(L, p, x):
assert p != NIL
n = new Node(x)
n.next = p.next
p.next = n
if L.tail == p: L.tail = n
L.size = L.size + 1Delete front (pop-front)
function POP_FRONT(L):
if L.head == NIL: return EMPTY
t = L.head
L.head = t.next
if L.head == NIL: L.tail = NIL
L.size = L.size - 1
destroy(t)Delete after node p
function ERASE_AFTER(L, p):
assert p != NIL
t = p.next
if t == NIL: return // nothing to erase
p.next = t.next
if L.tail == t: L.tail = p
L.size = L.size - 1
destroy(t)Delete first value k (search + unlink)
function ERASE_FIRST(L, k):
prev = NIL
curr = L.head
while curr != NIL and curr.value != k:
prev = curr
curr = curr.next
if curr == NIL: return NOT_FOUND
if prev == NIL: // deleting head
L.head = curr.next
if L.head == NIL: L.tail = NIL
else:
prev.next = curr.next
if curr == L.tail: L.tail = prev
L.size = L.size - 1
destroy(curr)Traversal skeletons
function FOR_EACH(L, f):
curr = L.head
while curr != NIL:
f(curr.value)
curr = curr.next
function FIND(L, pred):
idx = 0
for curr = L.head; curr != NIL; curr = curr.next:
if pred(curr.value): return (curr, idx)
idx = idx + 1
return (NIL, -1)Warning
Do not lose the list. When deleting
currduring traversal, savenxt = curr.nextbefore unlinking, then continue withcurr = nxt.
Example (Stepwise)
Start with empty list: head=tail=NIL.
-
PUSH_FRONT(3)→[3](head=tail=3). -
PUSH_BACK(5)→[3 → 5](tail=5). -
INSERT_AFTER(node(3), 4)→[3 → 4 → 5](tail=5). -
ERASE_FIRST(4)→[3 → 5]. -
POP_FRONT()→[5](head=tail=5). -
FOR_EACH(print)prints5.
Complexity and Performance
-
Traversal/Search:
O(n)time,O(1)extra space. -
Insert at head / insert after / delete at head / delete after:
O(1). -
Insert at tail:
O(1)withtail;O(n)without. -
Delete by value:
O(n)to find predecessor. -
Space:
Θ(n)nodes plus pointer overhead (one pointer per node in SLL).
Cache behavior. SLLs suffer from poor locality vs arrays; each step is a pointer chase. Arrays dominate when random access and iteration speed matter (see Dynamic Arrays).
Implementation Details or Trade-offs
Sentinel vs plain head
-
Plain head: Fewer objects; must special-case front operations when
head==NIL. -
Sentinel head:
headalways non-NIL; simplifies code paths (deletes/insert-at-front become “erase/insert after sentinel”). Slight memory overhead for one dummy node.
Tail and size fields
-
Tail enables
O(1)append and helps with queue-like workloads. Keeptailupdated whenever the last node changes. -
Size makes length queries
O(1)and aids invariants/tests; increment/decrement with each mutation.
Reversal in place (classic)
function REVERSE(L):
prev = NIL
curr = L.head
L.tail = L.head // tail becomes old head
while curr != NIL:
next = curr.next
curr.next = prev
prev = curr
curr = next
L.head = prevFind middle with slow/fast pointers
function MIDDLE(L):
slow = L.head
fast = L.head
while fast != NIL and fast.next != NIL:
slow = slow.next
fast = fast.next.next
return slow // middle (left-middle on even length)Cycle detection (Floyd’s tortoise & hare)
function HAS_CYCLE(L):
slow = L.head
fast = L.head
while fast != NIL and fast.next != NIL:
slow = slow.next
fast = fast.next.next
if slow == fast: return true
return falseTip
To locate the cycle entry after a meet: move one pointer to
head, advance both by one until they meet again; that node is the entry.
Memory management & safety
-
Manual memory (C/C++): Pair every
newwithdestroy. After unlinking a node, never read from it. Set itsnexttoNILbefore freeing to catch accidental double frees in debug builds. -
GC languages: Dropped nodes are collected automatically; be mindful of payload references if they are shared elsewhere.
Practical Use Cases
-
Queues / Producer–Consumer: SLL with
head/tailsupportsenqueue/dequeueinO(1)(see Queue). -
Adjacency lists: Graph representations often store neighbors in linked nodes when memory fragmentation is acceptable.
-
Hash table buckets: Separate chaining uses SLL per bucket (see Hash Tables).
-
Streaming logs / editors: Gap buffers and rope-like structures may embed linked chunks for insert-heavy workloads.
Limitations / Pitfalls
Warning
Random access is slow. No
A[i]. Accessing the i-th element isO(i). If you need indexing or range scans with locality, prefer arrays or balanced trees.
Warning
Lost head/tail. Always update
head/tailwhen removing or adding at ends; staletailcauses infinite loops or crashes.
Warning
Cycles (accidental or intended): Plain SLL algorithms assume acyclic lists; introduce cycle-aware variants (e.g., Floyd’s) when needed.
Warning
Concurrent mutation. Traversing while another thread mutates the list is unsafe without synchronization.
Warning
Space overhead. One pointer per node (two for DLLs) plus allocator metadata; for small payloads, overhead can dominate.
Warning
Comparator/equality. When removing by value, define value equality carefully (e.g., string content vs reference, floating-point tolerance).
Summary
Linked lists provide O(1) local updates with simple pointer rewiring and O(n) traversal. Their strengths are constant-time insert/delete given a predecessor, easy queue/stack patterns, and flexible growth without shifting or reallocation. Their weaknesses are poor cache locality, no random access, and higher per-element overhead. Use sentinels to simplify edge cases, maintain tail and size when useful, and rely on slow/fast pointers for middle finding and cycle detection. For heavy iteration or indexing workloads, arrays or trees are typically faster and simpler.