Suppose you have a sorted array of a million elements and you need to insert one value in the middle. The array has to shift half a million elements to make room — O(n) work, every time. A linked list inserts in O(1): update two pointers, done. That trade-off is the whole story.
The node structure
Every node holds two things: a value, and the address of the next node.
The last node's next is NULL — that's the sentinel that stops traversal.
The self-referential pointer struct Node *next uses the struct tag, not the
typedef name. That's not a style choice — it's required. The typedef alias
Node isn't complete until the closing brace, so the compiler doesn't know
its size yet. struct Node * is always a pointer to an incomplete type,
which is legal; a value of incomplete type is not.
malloc call — a separate heap allocation with its own
address. Nodes are almost never adjacent in memory. The "list" is a logical structure
built entirely from pointer links, not physical proximity.
Insertion and traversal
Inserting at the front is O(1): make a node, point its next at the old head, return it as the new head. Appending at the back is O(n): you must walk to the end first.
Deletion
Deletion is where linked lists get subtle. Removing node B from A→B→C requires
pointing A's next at C, then freeing B. The problem: you need A (the
predecessor) to do that. If you only have a pointer to B, you can't unlink it from
a singly linked list — you'd have to search from the head.
free(target), target is a dangling pointer.
Update prev->next first, then free.
Freeing the whole list
Freeing only the head leaves the rest of the nodes as leaked memory — they have no
other reference and can never be freed. You must walk the chain and free every node.
The standard pattern saves next before freeing, because once you free
a node you can't dereference it:
free(cur); cur = cur->next;After
free(cur), cur is freed memory.
Reading cur->next is undefined behavior — it might work, it might crash,
it might silently corrupt something else.
Doubly linked lists
A singly linked list can only move forward. Add a prev pointer and you get
a doubly linked list — backward traversal and O(1) deletion without needing the predecessor.
The price is two pointers of overhead per node and more bookkeeping on every insert and delete.
The Linux kernel uses doubly linked lists internally for almost everything — runqueues,
page lists, inode lists. They use a "intrusive" variant where the prev/next
pointers live inside the data struct itself, not in a separate wrapper node.
Arrays vs linked lists — the real trade-offs
| operation | array | linked list |
|---|---|---|
| access by index | O(1) | O(n) |
| prepend | O(n) shift | O(1) |
| insert (at known node) | O(n) shift | O(1) |
| delete (at known node) | O(n) shift | O(1)* |
| search | O(n) | O(n) |
| cache behavior | excellent | poor |
The cache row matters more than it looks. A modern CPU loads memory in 64-byte cache lines. When you iterate an array, the prefetcher sees the sequential access pattern and loads the next line before you need it — effectively free. When you follow a linked list's pointers, each node can be anywhere in the heap, so every dereference may cause a cache miss. At hundreds of nanoseconds per miss, the asymptotic advantage disappears fast.
The visualization
The viz shows a singly linked list being built node by node, then traversed.
Each node is a separate heap allocation — notice the non-sequential addresses.
The traversal cursor (cur) walks through the chain one pointer follow at a time.
A linked list trades cache-friendly contiguity for O(1) insertion at a known position — the O(1) only matters if you already have the pointer.