python/volume-4/chapter-50ch 50 / 64 · 9 min

50The linked list — data joined by pointers

In Chapter 49 we built the array: one contiguous block, where order is nothing but adjacency. Element 5 sits right after element 4, and "next" means a few bytes further along. The linked list is its exact opposite. I want to take the trade slowly, because it's the cleanest example in the whole volume of buying one power by selling another. Here's the plan. We scatter each element into its own little object on the heap — a node — and hand every node a pointer to where the next one lives. Order stops being about addresses and starts being about arrows. The whole way through, we keep asking the one question that decides everything: if the sequence lives in the pointers instead of the layout, what does that make almost free, and what does it make ruinously expensive? By the end you'll know why splicing into a linked list is O(1) when you already hold the spot, why finding that spot is O(n), and why a scattered list crawls even though it's "the same" O(n) as an array. You'll also know when that trade is the one you actually want.

Order without neighbours The pointer tax Insertion is two pointers — given the node …but finding it is a walk
scroll to begin

01Order without neighbours

Let's start with how an array keeps order, because the linked list is defined entirely by rejecting it. An array keeps order by adjacency: element 5 sits right after element 4 in one block, so "next" just means "8 bytes further along" (Volume 1, chapter 8). A linked list throws that rule out. It scatters its elements anywhere the heap has room. Each element carries a note — a reference, the same 8-byte machine pointer that names use in Volume 1 — saying where the next one is. The container remembers just one thing: a head reference to the first node. Follow the notes and the sequence unspools, even though no two nodes need sit anywhere near each other in memory.

Read that slowly, because it's the whole idea, and everything else in the chapter falls straight out of it: logical order lives in the pointers, not in the addresses. Two nodes that are "adjacent" in your playlist can sit on opposite ends of RAM. That one decision buys the linked list its superpower: you can rearrange the sequence without moving a single byte of data. It also saddles the list with its curse: you can't jump to the middle, because there's no address to compute. Let's watch it happen in memory first.

head a reference node @ 0x…c0 value next node @ 0x…14 value next node @ 0x…9c value next None ⏚ "Levels" "Wake Me Up" "Titanium" Blue = a reference to the next node · amber = a reference to the value · the arrows, not the addresses, hold the order.
Fig — A singly linked list of three songs. Each node is a two-cell box: a reference to its value and a reference to the next node. The nodes sit at unrelated addresses (…c0, …14, …9c); the sequence exists only because the blue arrows chain them, ending at None.

If order is just arrows, rearranging the playlist should be almost free — no block to shift. Let's cash that in. But first: what does one node actually cost? →

02The pointer tax

Nothing is free, and the linked list's bill comes due in bytes. In an array (a Python list), each element is stored as one bare 8-byte reference, packed shoulder-to-shoulder in a shared block. The object header is paid once for the whole list. A linked list can't do that. Every element is its own heap object, so every element pays the full Python object tax again. I measured a minimal node on CPython 3.12 — a class with __slots__, so it carries no per-instance dictionary:

cost.pypython
import sys

class Node:
    __slots__ = ('value', 'next')      # no per-instance __dict__
    def __init__(self, value):
        self.value = value             # a reference to the song
        self.next  = None              # a reference to the next node (or None)

a = Node("Levels"); b = Node("Wake Me Up"); c = Node("Titanium")
a.next = b; b.next = c                  # a -> b -> c -> None
print(sys.getsizeof(a))                 # 48   bytes per node

48 bytes. The node holds only two useful references — 16 bytes of actual pointers — wrapped in 32 bytes of Python machinery: the reference count, the type pointer, and the garbage-collector bookkeeping every tracked object carries (Volume 1, chapter 4). So to store one element's worth of sequence, an array spends 8 bytes and a linked list spends 48. That's a 6× memory tax — 40 extra bytes on every single node. That surcharge is what buys the flexible arrows, and it is never free.

The tax is per element, so it never stays small; it scales with your data. Drive the count up and watch the same numbers cost three wildly different amounts of memory, decided by nothing but how you wrap them. Watch, too, where each layout spills out of cache.

InteractiveStore a million numbers — pay the pointer tax
the same N numbers, stored three ways — what does the sequence itself cost? N = 1,000,000 numbers values are shared by all three — this is the container's own plumbing array 8 B / elem 8.0 MB · fits L3 ✓ slotted node 48 B / elem 48 MB · spills ✗ un-slotted node 344 B / elem · no __slots__ 344 MB · spills ✗ UN-SLOTTED LIST · PLUMBING FOR THE SAME NUMBERS 344 MB = 43× the array's 8.0 MB — for identical data At 1,000,000 numbers the array's links still fit a 32 MB L3 cache; the un-slotted nodes miss it by 11×.
1,000,000
Slide from a thousand to ten million. The array keeps its sequence in 8 bytes/element; a slotted node pays 48, an un-slotted node 344 — a 43× blow-up for the identical numbers, almost all of it Python bookkeeping the CPU never reads. Notice who still fits a 32 MB cache and who doesn't: layout alone decides.
one linked-list node — 48 bytes object header · 32 B value ref8 B next ref8 B 40 B tax one array slot — 8 bytes value ref8 B …header paid once for the whole block
Fig — The pointer tax. A node spends 48 bytes to carry the same 8-byte reference an array slot holds bare — 40 bytes of per-element overhead (a 32-byte header plus the extra next pointer). Multiply by a million nodes and it's real memory.
Why 32 bytes of header, when Volume 1 said ~28?
Volume 1's "~28-byte header" was measured on a small int (sys.getsizeof(5) is 28) — that figure folds in the int's own digit storage. Our node has no payload of its own beyond two references, so it shows the header alone: 16 bytes of core object (the reference count and the type pointer) plus a 16-byte garbage-collector head that every container-like object drags along so the cycle collector can find it. That's 32 bytes of pure Python bookkeeping before a single useful pointer. Both numbers are honest; they're measuring different objects.
The 344-byte node — a tax you can accidentally 7×
Drop the __slots__ line and every node grows a __dict__ to hold its attributes. I measured it: the same node balloons from 48 bytes to 344sys.getsizeof(fn) + sys.getsizeof(fn.__dict__). Seven times heavier, for identical data. If you ever hand-roll a node-based structure in Python, __slots__ is not optional; it's the difference between a lean list and one that evicts your working set from cache. Most of what's "slow about linked lists in Python" is really the un-slotted node.

So a node is heavy but its arrows are flexible. Now spend that flexibility — insert a song mid-playlist and count what actually moves. →

03Insertion is two pointers — given the node

Here's the linked list's marquee trick. To insert a new node X between two existing nodes A and B, you do exactly two things: point X.next at B, then point A.next at X. Two reference writes. Nothing else in the entire structure is read or moved — not the million nodes before A, not the million after B. Compare an array, where inserting at position i must shift every element from i onward one slot down to open a gap (Volume 1, chapter 8). That's O(n) copies. The linked list splice is O(1), and here's the beautiful part: it stays O(1) no matter where in the sequence you splice, because the cost doesn't depend on position at all. Order is arrows, and you're just re-drawing two of them.

A B C old A.next (discarded) B.next — untouched X (new) ① A.next = X ② X.next = B Two writes. Zero elements moved. And it's the same two writes whether the list holds 3 nodes or 3 million.
Fig — A splice rewires exactly two pointers: A.next = X and X.next = B. The old A→B arrow is simply dropped. Constant work, independent of list length or position.

The widget makes it literal. Slide to pick where the new node goes and watch. The linked list always writes two pointers, while the array beside it must shift a different number of elements depending on where you cut.

InteractiveSplice a node in — watch exactly two pointers rewire
2
The linked list writes 2 pointers at every gap — O(1). The array's shift count changes with position — O(n). Same edit, different cost.
↺ The thing people get backwards
"Insertion in a linked list is O(1)" is the most over-quoted half-truth in data structures. It's only O(1) once you already hold the node to splice at — handed to you by an iterator, a hash-map entry, or an LRU handle. If you have to find that spot first — "insert after the node whose value is Titanium", or "insert at index 500" — you pay O(n) to walk there, and the walk dominates. So mid-list insertion, start to finish, is O(n) for a linked list too. The linked list doesn't make insertion cheap; it makes it cheap given a reference to the location. The whole game is arranging for something else to already own that reference.

Which raises the obvious question: if I only have an index — "give me the 500th song" — how does a linked list find it? →

04…but finding it is a walk

Ask an array for element 500 and it does one multiplication: base + 500 × 8, jump straight there, done. That's O(1), the reward for living in one block (Volume 1, chapter 8). A linked list can offer nothing like it. There is no base address and no arithmetic, because the nodes are scattered and only the arrows know the order. To reach index k you must start at head and follow next k times, node by node. Reaching the 500th node means visiting the other 500 first. That's O(n), and it's baked into the layout — the price of trading adjacency for arrows.

I measured the gap on a million-node list. Walking to the middle took about 9 milliseconds; the array's lst[k] took about 0.1 microseconds — the O(n) walk versus the O(1) address computation. Most of those 9 ms is simply the half-million pointer hops the walk is forced to make. The array never hops at all. (Wall-clock numbers vary by machine; the O(n)-vs-O(1) shape does not.)

linked list: walk from head, hop by hop 0 1 2 k hop · hop · hop … k times → O(n) array: one multiply-and-add base + k×8 → O(1)
Fig — The mirror image of insertion. The array indexes in one arithmetic step; the linked list must walk k hops from the head because only the arrows encode position. Great at splicing, slow at seeking.

Drive the walk yourself. Slide the target index and watch the current pointer crawl from the head, one hop per node. The hop count is the index.

InteractiveWalk to index k — count the hops
0
Reaching index k costs k hops from the head — an array would reach it in zero. This is why random access on a linked list is O(n).

Myth

"Linked lists beat arrays because you never shift elements — so use them when you insert a lot."

Reality

Only if you already hold the node. If you insert "after the item I'm looking at while iterating", the linked list wins. If you insert "at index i" or "after the item with value v", you pay O(n) to find the spot — and then the array's fast index and cache locality usually make it the better choice anyway.

O(n) to walk, O(n) to scan — the same big-O as an array. So a full pass over a linked list and a full pass over an array should take the same time, right? Time them and prepare for a shock. →

05Same O(n), wildly different speed

This is the section that separates people who know Big-O from people who know the machine. Summing a linked list and summing an array are both O(n): n additions, one per element. Big-O says they should scale identically, and they do. But wall-clock time is a different story, and the reason is the memory hierarchy from Volume 3. The CPU never fetches one value from RAM. It fetches a whole cache line — 64 bytes, eight references' worth — and keeps it in fast on-chip memory. An array is one contiguous block, so a single fetch pulls in the next eight elements for free. The scan streams: fetch a line, use eight, fetch the next. This is called spatial locality, and hardware is built to reward it.

A linked list has none of it. Its nodes are scattered wherever the allocator found room, so following next lands you at an unrelated address every time. Each hop is a fresh cache miss: the CPU stalls, waits a hundred-odd cycles for RAM, pulls a 64-byte line to use just 8 bytes of it, then hops somewhere else and does it again. This is pointer-chasing, and it's the linked list's hidden tax. I measured a two-million-element sum three ways:

cache.pypython
array sum (contiguous)                     49 ms   (24 ns/elem)
linked list sum (nodes in alloc order)     55 ms   (28 ns/elem)
linked list sum (nodes shuffled in RAM)   407 ms  (204 ns/elem)

Same n, same additions, same O(n). Yet the scattered linked list ran roughly 8× slower than the array on that run, and about 6× on a second run. (It's a cache effect, so the exact ratio drifts with the machine and memory state, but it's always a large constant, never 1×.) Look closely at the middle row. A linked list whose nodes happen to be allocated in order is nearly as fast as the array, because consecutive nodes land near each other and the scan stays cache-friendly. It's the scattering that kills it, not the linking. Two structures, one complexity class, an order-of-magnitude difference in the only number the user feels.

array: one 64-byte line = 8 useful refs r r r r r r r r 1 miss → 8 used ✓ scattered list: one line = one node, 7/8 wasted nodewasted nodewasted nodewasted 1 miss per hop → the CPU stalls, over and over
Fig — Why the same O(n) diverges. The array's contiguous block turns one memory fetch into eight useful elements; the scattered list burns a whole cache-line fetch per node and stalls on every hop. Cache locality is the difference between 24 ns and 200 ns per element.
Big-O hides the constant that ate your afternoon
Two algorithms with the same Big-O can differ by 10× in practice, and cache locality is the usual culprit. This is exactly why Python's own list and deque — both built on contiguous blocks of references — beat a hand-rolled node-per-item linked list for almost every real workload, even the ones textbooks say "favour" linked lists. When you profile and two "equally fast" options aren't, suspect the memory layout before you suspect the algorithm.

If it's heavier and slower to scan, why does the linked idea sit at the heart of some of the most-used software on your machine? Because there's a job only it can do. →

06Two-way streets, and where this really lives

Add one more pointer to each node — a prev reference back to the previous node — and you get a doubly linked list. Now you can walk either direction. More importantly, you can delete a node in place when you're standing on it: reach both neighbours through prev and next, wire them to each other, and the node is gone in O(1), with no need to have walked from the head to find the one before it. My measurement puts a doubly linked node at 56 bytes versus the singly node's 48: one extra 8-byte pointer, the fee for two-way travel. Link the tail's next back to the head and it becomes circular — no beginning, no end, useful for round-robins.

prev · val · next"Levels" prev · val · next"Wake Me Up" prev · val · next"Titanium" next → ← prev With prev and next, deleting the middle node is O(1) from the node itself — wire its two neighbours together.
Fig — A doubly linked node carries prev and next (56 bytes vs 48). Two-way arrows let you delete a node you're standing on in O(1), without ever walking from the head.

You almost never type class Node in Python. The built-in list and deque (Chapter 53) cover the common needs faster, thanks to their block layout. The linked list's real importance is that the idea is everywhere underneath, doing the one job it owns: rearranging a sequence, or splicing and unsplicing elements, in O(1) when you hold a handle to the spot. It does this with no reallocation, and node identities stay valid while the structure changes around them.

Where you meet this — every day, mostly hidden
An LRU cache — the thing that decides what your OS, your browser, and your database keep in fast memory — is a hash map paired with a doubly linked list: a cache hit splices its node to the front in O(1); the least-recently-used item falls off the back. Python's own functools.lru_cache and OrderedDict are built exactly this way. The Linux kernel threads a circular doubly linked list (list_head) through nearly everything — process tables, scheduler run-queues. A memory allocator tracks free blocks as a linked free list. A blockchain is a backward linked list — each block points to the previous one's hash — and so is a chain of git commits, each pointing at its parent. Even the classic FAT filesystem stored each file as a linked list of disk clusters. You used an LRU cache to load this page.
The one-line tell for reaching for the linked idea
Being good at data structures is mostly matching an access pattern to a layout. Reach for a linked structure when your pattern is: I hold a reference to a position, and I splice or unsplice there repeatedly, and I rarely need to jump to element i. LRU eviction, an event list you thread and re-thread, a free list, adjacency lists in a graph (Chapter 60), the collision chains inside a hash table (Chapter 54) — all that shape. If instead your pattern is "give me element i" or "scan it fast", the contiguous array wins on both counts, and you should feel the pull toward a list or deque.
The deeper cut
The linked list is one of computing's oldest data structures — Newell, Shaw, and Simon built it into IPL around 1955–56, the list-processing language that seeded LISP. The insight was radical for the era: represent a structure's order with explicit links rather than physical adjacency, decoupling logical shape from memory layout. That decoupling is precisely what lets a tree (Chapter 56) or a graph (Chapter 60) exist at all — they're just nodes with several pointers each instead of one. A subtlety worth knowing: real systems often avoid the 48-byte-per-item tax with an intrusive list, where the next/prev links live inside the data object rather than in a separate wrapper node (the kernel's list_head is embedded in each struct). And an unrolled linked list — several elements packed contiguously per node — is the halfway house the deque takes to the extreme in Chapter 53, trading some pointer flexibility back for the cache locality you just watched matter so much.

One more thing only a linked structure gives cheaply: reversal. Flip every node's arrow and the sequence runs backward — no data copied, just pointers turned around. Watch it move only pointers:

arp.pypython
def reverse(head):
    prev = None
    cur  = head
    while cur is not None:
        nxt = cur.next     # remember where we were going
        cur.next = prev    # flip this node's arrow backward
        prev = cur         # advance the two walkers
        cur = nxt
    return prev            # prev is the new head

# a -> b -> c  becomes  c -> b -> a
# before: ['Levels', 'Wake Me Up', 'Titanium']
# after : ['Titanium', 'Wake Me Up', 'Levels']

Line by line: prev trails behind and cur leads. Each turn of the loop stashes the forward link in nxt, or we'd lose the rest of the list the instant we overwrite it. Then it flips cur.next to point backward at prev, and shuffles both walkers one step forward. When cur runs off the end, prev is left holding the last node — the new head. I ran it on Levels → Wake Me Up → Titanium and got Titanium → Wake Me Up → Levels. Not a single value object moved in memory; only the arrows turned. That is the linked list's whole personality in eight lines: cheap to re-wire, and it never touches the data.

Wait —
if splicing at a held node is the linked list's superpower, what if we deliberately only ever touch one end? Restrict it hard enough and you get a structure so useful it runs the call stack, every Undo button, and the Back button in your browser…

The next chapter does exactly that. Take a list, forbid every operation except push and pop at one end, and you get the stack — the simplest container there is, and the one quietly running your program's function calls right now. →

PROGRAMS run them line by line — the same ideas, written the many ways Python offers
Chapter 50, in working code

Twelve tiny linked lists you can actually run. We build the node from raw primitives, splice and seek and reverse it, then watch the very same O(n) cost a fortune in cache. Every number below is the machine's own, printed live.

Order lives in the arrows, not the addresses
Build the node from primitives, then walk it — the sequence exists only because each node points to the next, so we can reorder the whole playlist without moving a single value.
Splice is cheap, seek is dear
The linked list's personality in three runs: inserting given the node is a flat two writes, but reaching index k, or the tail, is a hop-by-hop walk — O(1) one way, O(n) the other.
The hidden bills — bytes and cache
Same data, two layouts. A node pays a 6x memory tax for its flexible arrows, and a scattered scan pays it again in cache misses — the constant Big-O quietly throws away.
Two-way streets, and where this really lives
Add a prev pointer and close the ring, and the linked idea shows its real face — reverse in place, delete where you stand, cycle forever, and run the LRU cache that loaded this page.
end of chapter 50 · six sections
iolinked.com
Written by Ajai Raj