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

62The decision map — pick the right structure on sight

In Chapter 61 we built the thirteenth and last container — Union-Find. That fills the volume's shelf: thirteen structures, thirteen chapters. This capstone doesn't add a fourteenth. Instead it teaches the one skill the other thirteen were quietly building toward — how to choose a structure on sight, the way a doctor reads a chart. Here's the plan. We lay all thirteen on a single master table, draw the flowchart that turns an access pattern into a structure, then throw the table away. The real move isn't memorising it; it's deriving it. All the way through we keep asking the one question that turns thirteen facts into a single reflex — what do I do to this data most, and which shape in memory makes that operation free? By the end you'll meet a fresh problem, name the operation you repeat most, and read the structure straight off it. Then we close the series — from a single bit in Volume 1 to the containers that organise a program's whole world.

Every structure is one answer to one question The master table — thirteen structures on one page The decision flowchart — from access pattern to structure Same operation, every structure — watch the costs sort out
scroll to begin

01Every structure is one answer to one question

Rewind to the promise this volume opened with: a data structure is a deal you strike with memory — spend a little space here to make an operation instant there. Watch how every chapter signed that same deal differently. The array laid its elements in one contiguous block so that position i is a multiply-add away — O(1) index — and paid for it with O(n) inserts in the middle. The hash map scattered its entries across a sparse table so a key becomes an address — O(1) lookup — and paid with cache misses and empty slots. The heap folded a tree into a flat array so the minimum sits at index 0. And Union-Find — the structure we just left — shrank "are these connected?" to a walk up a parent-pointer forest flattened almost to nothing. Different shapes in RAM, each bending the cost of one operation down to the floor.

So the thirteen aren't thirteen facts to hoard. They're five shapes memory can take — a row, a grid, a sparse table, a hierarchy, a web — and a handful of variations on each. Hold the five shapes in your head and the whole volume collapses into something you can see.

the five shapes every structure in this volume takes in RAM LINEAR one contiguous block array · stack · queue · deque base+0 +8 +16 +24 +32 +40 a b c d e f addr = base + i·8 GRID a flattened table matrix row after row KEYED a sparse table hash map · set key hash v v v mostly empty HIERARCHY nodes+pointers, or a flat array tree · BST · trie · heap pointer tree 0 1 2 3 4 same tree, no pointers child = 2i+1 NETWORK nodes joined by edges graph · union-find who touches whom — adjacency, not geometry parent[]
Fig — the whole volume in one picture. Five shapes memory can take. Every structure you met is one of these bent to make a single operation O(1): a row for position, a grid for coordinates, a sparse table for keys, a hierarchy (as scattered nodes or a flat array) for order, a web for relationships.
↺ The thing people get backwards
Beginners think "good at data structures" means knowing more structures. It doesn't. Every structure here is common knowledge; you could look each up in a minute. The scarce skill is the opposite direction — looking at a problem and knowing which shape it wants. The structure isn't the thing you memorise; it's the consequence of one question you learn to ask. The rest of this chapter is that question.

First, though, let's lay all thirteen side by side — one page you could pin above a desk. Not to memorise, but so the pattern behind it becomes impossible to miss. →

02The master table — thirteen structures on one page

Here is the whole volume as a lookup. Read each row as a profile: what shape it takes in RAM, what one slot costs, and — the columns that matter — which operations it makes cheap and which it makes expensive. The green cells are what each structure is for; the red-ish O(n) cells are the price it charges everywhere else. No structure is green across the board. That's the entire point: every structure is fast at something by being slow at something else.

StructureShape in RAMSpace / item*Index [i]SearchInsertDeleteMin / MaxBuilt for
Array (list)contiguous refs~8 BO(1)O(n)O(1)* end · O(n) midO(1) end · O(n) midO(n)index by position; iterate in order
Linked listscattered nodes + pointers~48 B/nodeO(n)O(n)O(1) at a nodeO(1) at a nodeO(n)splice with no shifting
Stackarray / linked, one end~8 BO(n)O(1) pushO(1) popO(n)LIFO — newest first (undo)
Queuering / linked~8 BO(n)O(1) enqueueO(1) dequeueO(n)FIFO — arrival order
Dequelinked fixed blocks~8 BO(n)O(n)O(1) both endsO(1) both endsO(n)fast at both ends
Hash map (dict)sparse hash table~30 B tbl (~93 all-in)O(n) by valueO(1)*O(1)*O(n)O(1) lookup by key
Matrixflat row-major grid1–8 B/cellO(1) [i][j]O(n²)O(1) setO(1) setO(n²)grid by coordinate; dense numbers
Binary treescattered nodes, 2 pointers~56 B/nodeO(n)O(n)O(1) at a nodeO(1) at a nodeO(n)model a hierarchy
Balanced BSTordered nodes~56 B/nodeO(log n)†O(log n)O(log n)O(log n)O(log n)ordered: search + range + sorted walk
Heapflat array, implicit tree~8 BO(1) slotO(n)O(log n) pushO(log n) popO(1) peekthe min/max, over and over
Trienodes, child-map per charheavy / nodeO(L)O(L)O(L)O(L)O(n)prefix search; autocomplete
Graphadj-list dict / matrixO(V+E) / O(V²)O(V+E)O(1) add edgeO(deg)relationships; traversal
Union-Findflat parent array~4–8 BO(α)O(α) unionconnectivity; grouping
How to read the O's — and the fine print
* amortised: O(1)* means "O(1) on average once you spread the occasional expensive resize/rehash across many operations" (Vol 3's amortised analysis). a plain BST has no index-by-position; an order-statistic variant gives O(log n). α is the inverse Ackermann function — measured effectively ≤ 4 for any n that fits in the observable universe, i.e. a constant in all but name. "Space / item" counts the structure's own slots, not the element objects it references — and every Python object carries a ~28-byte header of its own (Vol 1).
Amortised O(1) is not worst-case O(1)
That asterisk hides a spike. A list append is O(1) on average, but the one append that overflows the block copies every element to a bigger block — a single O(n) stall. A dict insert that triggers a rehash does the same. For throughput it averages out; for tail latency — the p99 a real-time system is judged on — that rare spike is exactly what bites. When latency must be predictable, weigh a structure's worst case, not just its average.

Those space numbers aren't guesses — they're measured. The container's own overhead per element, on CPython 3.12, spans a striking 50× range: a packed byte for a bytearray, up through the 8-byte reference of a list, to the ~93 all-in bytes a dict spends to buy you O(1) keys.

table.pypython
import sys
from collections import deque

sys.getsizeof([])                                        # -> 56    empty list = just the header
(sys.getsizeof(list(range(10000)))  - 56) / 10000        # -> 8.0   B/item  · one 8-byte reference
(sys.getsizeof(deque()))                                 # -> 760   a deque pre-allocates a block
(sys.getsizeof({i:i for i in range(10000)}) - 64)/10000  # -> 29.5  B/item  · sparse-table slots only
(sys.getsizeof(set(range(10000)))  - 216) / 10000        # -> 52.4  B/item  · a set is even sparser

Line by line: an empty list is 56 bytes of header and nothing else. Grow it to ten thousand elements and each one adds exactly 8.0 bytes — the contiguous array of references we built back in the array chapter, over-allocation already amortised in. A deque starts at 760 bytes because it eagerly grabs a whole memory block to make both ends O(1). The dict spends 29.5 bytes per entry on table machinery alone — and once you add the boxed key and value objects (~32 B each for a big int), it's ~93 bytes all-in to record one key→value pair. That's the price of O(1) by key: emptiness. A hash table must stay mostly empty to stay fast.

measured container overhead — bytes per element (CPython 3.12) bytearray / uint8 1.0 B array('i') 4.0 B list · deque · heap 8.0 B dict (table only) 29.5 B linked-list node 48 B + data set 52.4 B dict (all-in, big keys) ~93 B light = packed values in a block · heavy = pointers, headers, and the empty slots a hash table needs the deal, made visible: you pay in bytes for the operation you made free
Fig — the space half of every deal. Real sys.getsizeof numbers. Contiguous packed data is featherweight; the structures that buy O(1)-by-key or O(1)-splice pay for it in headers, pointers, and deliberate emptiness. Speed and space are the two sides of the same coin — you never get both.

A table is a lookup, and lookups are for people who already know what they want. The harder skill is arriving with a vague problem and walking out with a structure. That's a flowchart — and here it is, made clickable. →

03The decision flowchart — from access pattern to structure

Forget the thirteen names for a moment and ask a single question about your data: what do I do to it most? Not occasionally — most, in the hot loop, a million times. Every branch below is a different answer to that one question, and each lands on the structure whose layout makes that answer O(1) (or as close as the problem allows). Read it top-down: identify your dominant access pattern, follow the branch, arrive at the container.

What do you access by? — the thing you do most — position / order of arrival any position →array last-in-first-out →stack first-in-first-out →queue both ends →deque a row in memory a key / label plain key →hash map string + prefix →trie a sparse table rank / order the min/max, repeatedly →heap sorted + range →balanced BST a tree — nodes or flat array coordinate / relation 2-D grid →matrix edges / paths →graph just "same group?" →union-find the branch you take = the operation you do most = the layout that makes it O(1) everyday translations: your to-do list= a queue Ctrl-Z undo= a stack contacts app= a hash map your files= a tree friends= a graph
Fig — the decision map. One root question, five branches by access pattern, thirteen leaves. You don't walk it by recalling structures — you walk it by naming the operation you repeat most. The bottom row is the same map applied to your own day.

Now make it yours. Below, toggle the operation you'd do most and the flowchart hands back the structure — plus the one-line reason its layout wins. There's no "best" button, because there is no best structure. There's only the one whose shape makes your hot operation free.

InteractiveName your dominant operation — meet your structure
Every answer is the same shape: an operation → the layout that makes it O(1) → the structure that has that layout.

The flowchart tells you which structure. But to trust it, you have to feel why the same operation can be free in one and ruinous in another. Pick an operation and watch all thirteen sort themselves. →

04Same operation, every structure — watch the costs sort out

Here's the claim the whole table rests on: costs aren't assigned, they're forced by the layout. "Get the item at position i" is O(1) on an array because the address is arithmetic — base + i·8, computed and read in one step. It's O(n) on a linked list because position isn't stored anywhere; the only way to reach the i-th node is to start at the head and follow next i times. Same operation, same name, two layouts — and the layout, not the operation, decides the price.

array — one formula i base + i·8 → read O(1) linked list — i hops h i follow next, i times O(n) the operation is identical. the layout writes the bill.
Fig — cost is a property of layout, not of the verb. "Index i" costs one address computation on a contiguous array and i pointer-follows on a linked list. Derive the Big-O by asking how the shape lets you reach the data — never by memorising.
InteractivePick an operation — rank all seven structures by cost
The same structure tops one race and sinks another — proof there is no universally "fast" container, only fitness for an access pattern.

Play with it and the punchline lands: the array wins "index i" and loses "insert at front"; the hash map wins "by key" and can't even offer "position i"; the heap owns "remove the minimum" and is useless for membership. There is no row that's green everywhere. Choosing a structure is choosing which column you want green and which you can afford to leave red.

The tiebreaker the table hides: cache locality (the metal)
Two structures can share a Big-O and still run at wildly different speeds, because the CPU reads memory in cache lines, not bytes (Vol 3 ch2). A contiguous array streams — the prefetcher pulls the next line before you ask. A linked list stalls — each next is a jump to an unrelated address the prefetcher can't guess. Measured here: summing a contiguous array('q') of a million ints versus walking a million-node linked chain — same O(n) — the chain ran about 2.4× slower (machine-dependent). When two structures tie on paper, the contiguous one usually wins on the metal. That's why a heap is a flat array and not a pointer tree.
The wrong structure doesn't just slow down — it changes the exponent
Pick the O(n) operation for your hot loop and an O(n) task quietly becomes O(n²). "Is this in my list?" a thousand times over a thousand-element list is a million comparisons; the same check against a set is a thousand. Measured: a worst-case x in list over 100k elements took ~0.64 s for 1000 checks; x in set did 100× more checks in ~0.003 s. Same intent, different structure, four orders of magnitude. This is the single most common real-world performance bug — and it's a data-structure bug, not an algorithm one.
ex2_btns.pypython
import timeit
# SAME job — remove from the front 2000 times — on two layouts:
timeit.timeit("q.pop(0)",    "q=list(range(100000))", number=2000)                  # -> ~0.33 s   list: O(n) shift
timeit.timeit("q.popleft()", "from collections import deque;"
                             "q=deque(range(300000))", number=2000)                  # -> ~5e-5 s   deque: O(1)

# SAME question — is it present? — scanned vs hashed:
timeit.timeit("99999 in c", "c=list(range(100000))", number=1000)                   # -> ~0.64 s   list: O(n)
timeit.timeit("99999 in c", "c=set(range(100000))",  number=100000)                 # -> ~0.003 s  set:  O(1)

Line by line, and every number here was RAN: popping the front of a list is O(n) because every remaining element shifts down one slot — ~0.33 s for 2000 pops. A deque does the same removal in O(1) by moving a pointer, not the data — about 6000× faster on this machine. Then membership: scanning a list for a value is O(n), and 1000 worst-case scans of 100k elements cost ~0.64 s; a set hashes straight to the answer, so a hundred times as many checks finish in ~0.003 s. Same goals, different layouts, and the gap is the difference between a snappy app and a hung one. (Timings are machine-dependent; the ratios are the point.)

InteractiveThe exponent you can feel — x in list vs x in set as n grows
same job — "have I seen this before?", once per item — while de-duplicating n items n = 31,623 items x in list · O(n²) 499.99 million comparisons x in set · O(n) 31.6 thousand lookups SAME RESULT · THE LIST DOES THIS MUCH MORE WORK 15,811× list: 3.20 s set: 1.3 ms double n and the list quadruples while the set only doubles — that widening gap IS the exponent.
31,623
Drag n up one machine at a time. The list's cost is quadratic — each new item scans everything before it — so it explodes; the set hashes straight to the answer and stays linear. Counts assume distinct items (worst case for the list scan); the set is O(1) per check regardless. Wall-clock uses this chapter's measured ~6.4 ns per list-compare (machine-dependent) — the ratio is the honest part. This is the "changes the exponent" bug from above, made draggable.

You've now seen the table, the flowchart, and the costs. Time to throw the table away — because the people who are genuinely good at this don't carry it in their heads at all. →

05You don't memorise the table — you derive it

Here's the 1% move, and it's smaller than you'd think. Faced with any problem, the expert doesn't scan a mental list of thirteen structures. They ask one question: "what's the operation I'll do most — and which layout makes that operation O(1)?" Answer it, and the structure is no longer a choice you agonise over; it's a consequence you read off. Need constant lookup by an id? A layout that turns a key into an address → hash map. Need the next-smallest thing again and again? A layout that keeps the minimum at the front → heap. Suggesting words as someone types? A layout with one node per letter → trie. The structure falls out of the operation.

the operation you repeat the layout that makes it free → the structure look up a user by id turn the key into a slot address hash map next task by priority keep the min at index 0 of an array heap suggest as you type one node per character of the prefix trie are two people linked? walk up a flattened parent forest union-find read left to right — the structure is the answer, never the starting point
Fig — the derivation, four times. Start with the operation, ask what layout makes it O(1), and the structure appears. Train this direction and you'll never again memorise a table — you'll regenerate it on demand.
The deeper cut — the best structures are often two structures welded together

Real systems rarely reach for one container. When a problem has two hot operations, you combine the two layouts that each make one of them O(1):

  • LRU cache — needs O(1) "look up by key" and O(1) "move this to most-recently-used." Answer: a hash map (key → node) welded to a doubly linked list (recency order). Each structure covers the other's weakness; that pairing is Python's own functools.lru_cache and every database page cache.
  • Priority queue with updates (Dijkstra, Vol 3) — a heap for O(log n) "next smallest," plus a hash map from node to its heap position so you can O(1)-find an entry to decrease its key.
  • Inverted index (every search engine) — a hash map from word to a sorted array of document ids; the map finds the word, the array supports fast intersection.

The move scales: name each hot operation, pick the layout that makes it O(1), then stitch. "Good at data structures" at the highest level is composition — knowing which two shapes cover each other's blind spots.

Where you meet this — you use every one of these, today, without naming it
Your phone's contacts are a hash map (type a name, jump straight to a number). Ctrl-Z is a stack; the pages behind your browser's back button are the same stack. Your OS runs programs from a queue; your to-do list is one too. Your files and folders are a tree; a spreadsheet recalculates over a dependency DAG. Google Maps routes you over a weighted graph; "people you may know" walks a friendship graph. Autocomplete is a trie; the emoji and word suggestions on your keyboard, too. You've been living inside these thirteen structures your whole computing life — this volume just gave them names and showed you their bones.

Myth

There's a "best" data structure — learn the fastest one and default to it. Lists are simple, so use lists; or dicts are powerful, so use dicts everywhere.

Reality

There is no fastest, only fittest for an access pattern. Every structure is fast at one thing by being slow at others. The skill isn't a favourite — it's the reflex to name your dominant operation and pick the layout that makes it free, accepting the costs elsewhere.

Wait —
if the structure is just a consequence of the operation, then "learning data structures" was never really about the structures at all. It was about learning to see the operation underneath a messy problem. So what does that skill unlock next?

One question — "what do I do most, and which layout makes it O(1)?" — is the entire volume, distilled. Which means you're ready for the last section: where a person goes once the containers stop being mysterious. →

06Where you go from here — a bit, a structure, a system

Step back and look at the whole climb. Volume 1 started at a single bit — a box in RAM that's 0 or 1 — and built up through bytes, addresses, references, and the heap until a Python object was something you could picture down to its header. Volume 2 made that machine program: files, modules, tests, concurrency. Volume 3 taught it to think: Big-O, the memory hierarchy, divide-and-conquer, greedy, DP, the algorithms that turn brute force into elegance. And this volume, Volume 4, gave those algorithms their containers — the thirteen shapes that decide, before a single line of logic runs, what's cheap and what's ruinous. Bit → byte → object → structure. You can now trace a program's data from the metal all the way up to the shape it lives in.

a bitVol 1 an objectVol 1–2 an algorithmVol 3 a structureVol 4 — you are here real systemsnext the whole series, one staircase
Fig — the staircase you just climbed. From a bit that's 0 or 1 to the structures that organise a program's world — and the next step out, where these thirteen become the load-bearing walls of real systems.

So where next? Outward, into systems built from exactly these parts. Open any database and you'll find our structures load-bearing: an index is a B-tree (a fat, disk-friendly BST) or a hash index; write-optimised stores like Cassandra and RocksDB layer LSM-trees over sorted arrays and heaps; a query planner walks a DAG. Step up to distributed systems and the same shapes reappear, hardened for scale: consistent hashing is a hash map wrapped around a ring so machines can join and leave without reshuffling the world; Merkle trees — hash trees — let Git, Bitcoin, and Amazon's Dynamo verify terabytes by comparing a single root hash; bloom filters trade a little accuracy for a set that fits in a breath of memory; CRDTs let two offline copies of a structure merge without conflict. None of these is new magic. Each is one of your thirteen, chosen because its layout makes the system's hottest operation cheap — the exact reasoning you now own.

And that's the quiet gift of this whole series. You didn't memorise thirteen containers or two dozen algorithms; you learned to look at a problem and see the shape it wants — the operation underneath, the layout that makes it free, the price you're willing to pay elsewhere. That reflex doesn't expire. It's the same move whether you're picking a Python dict, designing a database index, or sketching a system on a whiteboard for a million users. Start from a bit; ask what you do most; choose the shape that makes it O(1). You began this series not knowing what a variable really was. You're leaving it able to reason about the machine from the transistor to the distributed cluster — from first principles, all the way up. Go build something, and pick your structures on sight.

This closes Volume 4 — and the series. Your next move isn't another chapter here; it's a real system. Take the reflex you built and open the door marked databases and distributed systems: B-trees on disk, LSM-trees under your writes, hash rings across a fleet, Merkle trees keeping it all honest. Same thirteen shapes, bigger stage. You're ready. →

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

Thirteen structures, five shapes, one question — here each container is built from bare lists and dicts so you can watch the layout itself decide what is free and what is ruinous.

Linear — a row in memory
Array, stack, deque: lay values side by side in one block and position becomes arithmetic. The array reaches any index in a single jump; the stack and deque just choose which end they touch.
Keyed — a sparse table
Hash map, set, trie: give up contiguity to turn a key straight into an address. A hash lands you at a slot (collisions chain), a set answers 'present?' in one probe, and a trie spends one node per letter to make prefixes free.
Grid & order — coordinates and rank
Matrix, BST, heap: three ways to keep shape. The matrix flattens two dimensions into one row-major block; the BST keeps keys ordered so each compare halves the search; the heap folds a tree into a flat array with the minimum parked at index 0.
Network — nodes joined by edges
Graph and union-find store who-touches-whom, not geometry. A BFS fans out level by level over an adjacency map; union-find collapses 'same group?' to a short walk up a parent forest.
The one question — layout writes the bill
Same operation, two shapes. Reaching position i is one address computation on a contiguous array and i pointer-hops on a linked list — proof that cost is forced by the layout, never by the verb.
end of chapter 62 · six sections
iolinked.com
Written by Ajai Raj