◈ python mapVol 4 · Ch 48/62
Volume 4 Python, from the metal up · chapter 48

48What a data structure really is

In Volume 3 we learned to count the work an algorithm does. We could stare at a loop and know how its cost grows. Now we turn to the thing that cost is really made of: the data structure. There are thirteen structures ahead of us, and they're all carved from one idea. A structure is two things welded together. First, a way of arranging data in memory. Second, the set of operations that arrangement makes cheap. That's the whole subject. And the whole way through, we keep asking the one question that decides everything: where do the bytes physically sit, and what does that force each operation to cost? By the end you'll be able to pick up any structure and size it up fast. It might be one you've used for years, or one you meet cold on page 200. Either way you ask the three questions that matter: how is it laid out in RAM, what does each operation cost, and when is it the right tool. Get fluent at those three and you've got most of what "good at data structures" means.

iolinked · chapter 48 — the checkpoints6 steps
$ sections covered in What a data structure really is
01A data structure is a deal you strike with memory
02Memory is the substrate everything sits on
03The contract and the implementation are two different things
04We grade a structure by what its operations cost
05Same Big-O, different metal — and the humans who saw it
06The one skill: match the access pattern to the layout

01A data structure is a deal you strike with memory

Let's start with the oldest puzzle in the whole field, and notice it has nothing to do with code. A phone book lets you find a person's number in seconds, because you flip to the surname and you're done. But hand it a number and ask whose it is, and that same book turns to agony, since now you'd read every single line. Same data, same paper, yet the arrangement, sorted by name, made one question instant and left the other nearly impossible. That trade is the entire discipline. Every data structure is a deal: you spend a little space arranging things this way, and some operation you care about becomes cheap. In return, some other operation gets expensive. There's no free arrangement, only arrangements tuned to different questions.

★ YOU ALREADY RUN THIS · the-kitchen-draweryou have already designed a data structure, and you did it with your hands
Stand in your kitchen and look at where things live. Forks lie flat in a divided tray, because you grab one without looking. Mugs hang on hooks at chest height, because you reach for them half-asleep. The spices sit in a rack you scan jar by jar, because you never know which one tonight wants. Nothing in that room was placed by what it is. Every single thing was placed by how you reach for it. And you paid for the whole arrangement in advance, on some Saturday you have half-forgotten, so that every weekday morning since has cost you nothing. Cook in someone else's kitchen, with the same pots and the same knives, and you slow to a crawl.
forks in a divided tray — one grab, no lookingcontiguous slots · lst[i] · O(1)
reading the spice rack jar by jarlinear scan · x in list · O(n)
the labelled jar your hand goes straight tohash the key into an address · d[key] · O(1)
wedging a new divider into a packed drawerevery item after it shifts · insert(0, x) · O(n)
the Saturday you spent arranging it allthe cost paid once, up front, so the reads are free
pin it: a data structure is a promise about where things will be — and you always pay for that promise before you need it.

To make the deal concrete, you have to see the arrangement in memory. Remember from Volume 1: RAM is one enormous row of numbered boxes, each holding a byte, each with an address. A value never floats in the abstract — it sits at an address. So "arranging data" literally means deciding which addresses hold what, and how the pieces find each other. Take three songs and store them two ways. Pack them into one contiguous block — slot after slot, no gaps. Or scatter them anywhere and have each one hold a pointer, an address that points at the next. Same three songs; two completely different deals with memory.

Three songs, two deals with memory CONTIGUOUS ARRAY — one block, slots side by side addr 1000 addr 1008 addr 1016 ▸ Levels ▸ Wake Me Up ▸ Titanium index 2 lives at 1000 + 2×8 = 1016 → jump straight there. O(1). LINKED NODES — scattered anywhere, joined by pointers Levels @6280 next Wake Me Up @1032 next Titanium @4160 to reach song #3 you must hop through #1 then #2 — no shortcut. O(n).
Fig — The same three songs, two arrangements. Contiguous: slots are adjacent, so any index is one address calculation away. Linked: nodes hide anywhere in RAM and only the pointers know the order, so reaching the k-th one means walking k links. The layout — not the language — decides the cost.

Look at what the picture already tells you, before a single line of code. In the array, item #3 sits at a computable address, base + 2×8, so you leap to it in one step. In the linked version the nodes sit at unrelated addresses (@6280, @1032, @4160). The only thing that knows song #2 follows song #1 is the pointer stored inside song #1. Want the third song? You've no choice but to walk the chain. Two arrangements of identical data, and one made "jump to position k" instant while the other made it a walk. That gap isn't a Python quirk. It's forced by where the bytes physically sit.

Let's put real numbers on that leap, because the whole chapter turns on it. Say the array's first slot lives at address 1000, and each slot holds one 8-byte reference. Then item #1 is at 1000, item #2 at 1008, and item #3 at 1000 + 2×8 = 1016. Notice what you did to reach item #3. You multiplied once and added once, and it didn't matter whether you wanted item #3 or item #3000. The same two-step calculation lands you on any slot. The linked chain can't do this, because its nodes sit at addresses that have no pattern. There's nothing to compute, so you follow pointers one by one until you arrive.

Now put the same kind of number on the walk, so the contrast is exact. Suppose you want node #500 in the linked chain. The only route is through the 499 pointers ahead of it, so you make 500 hops to arrive. Want node #3000 instead? That's 3000 hops, and there is no shortcut to skip them. The array reached any slot with one multiply and one add, a fixed two steps, while the chain's cost is the position itself. That is the difference between O(1) and O(n), written in hops you can count.

If the arrangement lives in memory, we'd better be fluent in the memory it lives in. One page of Volume 1, refreshed →

02Memory is the substrate everything sits on

Every structure in this volume is built out of exactly three Volume 1 facts, so let's nail them. One: a name is an 8-byte reference. Writing x = song doesn't copy the song. It stores the song's address, and that address is 8 bytes because this is a 64-bit machine. Two: every Python object carries a header before its actual data. The header is a reference count and a pointer to its type, roughly 16 bytes of pure bookkeeping on top of whatever it holds. Three: objects live scattered on the heap. The things that organize them, like lists and dicts, are just other objects holding references to them. Data structures are almost never about the values. They're about the arrangement of references to the values.

Don't take my word for the sizes — measure them yourself. sys.getsizeof reports an object's byte cost, and the numbers come back strikingly regular:

substrate.pypython
import sys

sys.getsizeof(object())   # 16   — the bare header: refcount + type pointer
sys.getsizeof(1)          # 28   — that 16-byte header + the integer's digits
sys.getsizeof([])         # 56   — an empty list is not free: header + growth bookkeeping
sys.getsizeof(list(range(100)))   # 856

# so what does one more slot in a list actually cost?
(856 - 56) / 100          # 8.0  bytes per slot

Read it top to bottom. A bare object() is 16 bytes. That's the header alone, the tax every Python object pays. A small int is 28: the same header plus room for its digits. An empty list already costs 56 bytes, because a list is a real object with its own header and its own record of how much room it's holding. Fill it with 100 items and it's 856 bytes. The arithmetic on the last line is the punchline: (856 − 56) / 100 = 8.0. Each item you add costs the list exactly 8 bytes. Not the size of the value, but 8 bytes, the size of one reference. The list stores addresses, and the 28-byte integers themselves live elsewhere on the heap. A Python list is a contiguous array of 8-byte references, exactly as Volume 1 promised.

Where does that 28 for an int come from? It's worth breaking open, because it's the same header you'll meet on every object. 16 of those bytes are the header we just named: 8 for the reference count and 8 for the pointer to its type. That leaves 12 bytes, and those hold the integer's actual value plus a small record of how many digits it needs. Add them up: 16 + 12 = 28. So even a plain number is mostly bookkeeping, with the value tucked in at the end. That's the tax Python pays to make every object self-describing, and it's exactly why a list of numbers stores 8-byte addresses instead of the numbers themselves.

Here's the test that proves the point. Put the number 5 and the number 10**100 — a googol, 101 digits long — into the same list. The googol is a far heavier object out on the heap, yet each one costs the list the same 8 bytes, because the list only ever stores the address of a value, never the value itself. Swap a tiny int for that monster and the list's own size doesn't move a byte. That is what "a list of 8-byte references" really means, and it's why sys.getsizeof on the list stays flat while the objects it points at vary wildly in size.

A list is a block of references; the values live elsewhere list header 56 B: type · len · cap ref → •8B ref → •8B ref → •8B the heap — scattered int objects (28 B each) 200 247 245 one contiguous block values anywhere, in any order the allocator chose
Fig — The list is the blue block of 8-byte slots; each slot is a reference, not a value. The integers sit off on the heap. This is why "8 bytes per item" holds no matter how big the numbers get — you're paying for addresses, not contents.

There's a wrinkle worth knowing now, because it recurs all volume. That empty list was 56 bytes with room for zero items, so where does the first item's slot even come from? Watch the size jump as you append: 56, then 88, then it holds steady, then leaps to 120, then 184. The list grabs its slots in batches rather than one at a time, keeping spare capacity so most appends touch memory it already owns. That's the dynamic array trick, Volume 3's amortized O(1) append, and the very next chapter on the array takes it apart bolt by bolt.

Those jumps aren't random, and the arithmetic tells the story. Empty, the list is 56 bytes with zero slots. The first append pushes it to 88, and (88 − 56) / 8 = 4 — it grabbed room for four references at once, not one. It then holds at 88 through the next three appends, spending slots it already owns. When it fills, it leaps to 120, another four slots, then to 184, which is eight more (184 − 120 = 64, and 64 / 8 = 8). Each time it runs out, it roughly doubles the spare room. So appends feel free on average: you pay for a big batch once, then coast.

So why does doubling make appends average out to cheap? Follow the copies. Growing a full list of n items means allocating a bigger block and copying those n across, which is real work, O(n) for that single append. But it only happens when the list doubles: at sizes 1, 2, 4, 8, and so on. Add up every copy you'll ever pay across n appends and it comes to n + n/2 + n/4 + … ≈ 2n, which is about two copies per append. A rare big cost, spread thin over many cheap ones, lands at amortized O(1). That is exactly what the word amortized is doing.

The deeper cut — why not store the values in the list?
Some languages do exactly that: a C array of int packs the numbers themselves, back to back, 4 bytes each, no separate objects and no references. It's smaller and faster to stream — but every element must be the same fixed-size type, and you lose Python's "a list can hold anything" freedom. Python chose the flexible deal: a uniform array of same-sized references, so one list can hold an int, a string, and another list at once, because each slot is just an 8-byte address and the wildly different objects live off on the heap. That's a labeled simplification of a real tradeoff, not the last word — the matrix chapter and NumPy come back to the packed-values arrangement, which is why numeric arrays crush lists on large math.

We can see the layout and price the bytes. But a "stack," a "queue," a "map" — those are promises, not pictures. Where do the promises end and the pictures begin? →

03The contract and the implementation are two different things

Here's the distinction that separates people who use data structures from people who understand them: a structure has two layers. The contract, also called the abstract type, is the promise — the operations it offers and what they mean. A stack promises just three of them: push (add to the top), pop (remove the top), and peek (look at the top). It also swears the last thing in is the first thing out, and that's the whole promise. Notice that the contract says nothing about memory. The implementation is the arrangement that actually keeps that promise. You can back a stack with a contiguous array or with scattered linked nodes: same contract, but different pictures in RAM. And here's the whole point — those different pictures mean different costs.

Two words people mix up, so let's be precise. A node is a container that holds a value and the pointer(s) to its neighbours; the value is just the payload inside it. When we say a linked list is "scattered nodes," the scattering is the nodes; the values they point to are scattered too, but separately. The contract only ever talks about values ("give me the top"). The implementation is what decides whether getting that value is a leap or a walk.

InteractiveSame contract, swap the backing — watch the cost move
operation: get item at index k backing: contiguous array O(1) ≈ 38 ns measured address = base + k×8 — one jump, straight to the slot. the contract (what the operation promises) never changed — only the layout under it did.
operation
backing
Push/pop at the end is cheap on both — that's why the stack contract doesn't care. But index and remove-from-front swing wildly with the backing. The contract is the same; the bill is not.

The myth

"A stack is a Python list." "A queue is a linked list." The structure and one way of building it get treated as the same thing.

The reality

A stack is a contract: last-in-first-out, three operations. A list is just one implementation that honours it, and if you swap in another backing, every promise still holds while only the costs move. So name the contract and the implementation separately, always.

Before we grade costs, one quick refresher from Volume 3, because the next few pages lean on it. When we write O(1), we mean the work stays flat no matter how big the data gets. Reaching one array slot takes the same single step whether the list holds 10 items or 10 million. When we write O(n), we mean the work grows in step with the size. Walking a chain of n nodes takes n hops, so doubling the data doubles the work. That's the whole vocabulary you need here. O(1) doesn't flinch as the data grows, and O(n) grows right alongside it.

So the same operation can be O(1) or O(n) depending on the layout. Which means we can't judge a structure by vibes — we have to price it. Here's the price list, and why it reads the way it does →

04We grade a structure by what its operations cost

You choose a structure the way you'd choose a vehicle. Not by how it looks, but by what it makes cheap. For every structure we ask the cost of four operations, plus its space. We express each as a Big-O in n, Volume 3's yardstick: access (reach the k-th item), search (find a value), insert, and delete. The magic is that you can derive each cost by reasoning about the layout. You never have to memorize a table. Access on a contiguous array is O(1) because the address is base + k×8, one multiply-and-add regardless of k. Access on linked nodes is O(n) because the only route to node k is through the k−1 pointers before it. The layout forces the cost.

Access was one operation of the four. Insert and delete are where the two layouts really trade places, so let's derive them the same way. To insert at the front of a contiguous array, every existing item has to shift up one slot to make room, so the cost grows with the count: O(n). The linked chain does the opposite. To splice a new node in at the front, you point it at the old head and move the head, and nothing else budges: O(1). Delete flips the same way. The array closes the gap by shifting everything back down, while the chain just re-points one pointer. So neither layout wins outright. The array is fast to reach into and slow to reshape, and the chain is exactly the reverse.

That leaves the fifth thing we grade: space. Line the two layouts up for n items and the numbers fall out. The contiguous array is a block of n references, so its slots cost 8n bytes and nothing more. The linked chain stores the same n references, but each one now lives inside a node that also holds a next pointer, another 8 bytes. So every element carries 16 bytes of structure instead of 8, and the chain spends roughly double the memory to buy its cheap front-insert. Nothing here is free — the pointer you follow is a pointer you also paid to store.

Reach index 5 — one calculation vs five hops array base + 5×8 one jump → O(1) linked 5 hops, no shortcut walk the chain → O(n)
Fig — Same index, same n, opposite cost — read straight off the layout. Contiguous storage turns "position" into "address"; pointer storage turns it into a walk. You never memorize O(1) vs O(n); you see it.

Search shows the same logic, and it's the one you feel every day. Asking "is x in here?" of a plain list means scanning until you find it or run out, which is O(n). Asking the same of a set instead means computing x's hash into an address and looking in exactly one spot, which is O(1). Different arrangement — a sparse hash table from the set/dict internals in Volume 1 — asks the same identical question and gets a radically different cost. Timed on this machine:

How does a key "become an address"? Here's the move in miniature, with small numbers to make it checkable. Say the set has 8 slots, and you ask whether the number 91 is present. Python runs 91 through a hash function and then folds the result down to a slot with %, the remainder operator. Take the simplest case, where the hash of 91 is just 91: then 91 % 8 = 3, because 8 goes into 91 eleven times with 3 left over. So the set looks only in slot 3. It doesn't scan slots 0, 1, 2, and onward. It computes the one address that could hold 91 and checks that single spot. That's why the size of the set barely changes the cost. The answer is a calculation, not a search.

One fair question before we move on: what if two numbers want the same slot? Ask for 99 in that same 8-slot set, and 99 % 8 = 3 too, because 8 goes into 99 twelve times with 3 left over — the very slot 91 wanted. That's a collision, and the set handles it by checking slot 3, seeing the wrong value there, and stepping to the next slot until it finds the number or hits an empty gap. The trick that keeps this fast is keeping the table sparse: Python grows it well before it fills, so those little walks stay a step or two long, and the lookup is still O(1) on average.

costs.pypython
data  = list(range(100_000))    # the same values, two arrangements
sdata = set(data)
    target = 99_999   # a value near the end (worst case for the scan)

target in data      # scan every slot until found/absent  → O(n)
target in sdata     # hash the key, look at one bucket     → O(1)

# measured, best of many runs on this machine:
#   x in list  ≈ 565,000 ns   (half a millisecond)
#   x in set   ≈      51 ns   (~11,000× faster for the SAME question)

Line by line: data and sdata hold the exact same 100,000 numbers, and only the arrangement differs. The membership test reads identically in code (target in …), so the contract is the same, yet the list must walk its slots while the set jumps to a hashed address. The stopwatch reports the difference bluntly: about half a millisecond versus about fifty nanoseconds, which is roughly eleven thousand times faster. The exact ratio is machine-dependent, but the shape, O(n) versus O(1), is not. You didn't write smarter code here — you picked a smarter arrangement, and that is the entire skill in one line.

InteractiveSame question, two arrangements — watch the gap explode past every limit
one question — “is this value in here?” — asked of the same values, arranged two ways 100,000 values — identical in both LIST — scan slot by slot · O(n) ≈ 565.0 µs SET — one hash, one bucket · O(1) ≈ 51 ns SAME QUESTION · SAME VALUES · ONLY THE ARRANGEMENT DIFFERS — SO THE SET IS… 11,078× faster — for the identical work Scaled so the set answers in 1 second, the list would take 3.1 hours — same question. The gap has no ceiling: every 10× more data multiplies the set’s lead by 10. You chose the arrangement, not the code.
100,000
Drag from a handful up to a billion. Below about nine items the plain list scan actually wins — hashing isn’t worth the overhead yet. But the instant you cross that line the set pulls ahead and never stops: the win grows by 10× for every 10× more data, with no upper bound. That widening gap — not a cleverer loop — is what “pick a smarter arrangement” actually buys you.
Where you meet this — every second, invisibly
That set-versus-list gap is running under everything you touch. When your browser checks whether it's already seen a URL, when a spell-checker decides teh isn't a word, when a login server asks "is this session token valid?", when a game engine tests "did the ray hit any wall?" — none of them scan a list. They hash into a table and answer in one hop, billions of times a day, because the O(n) version would melt under the load. You've never noticed, which is the point: the right structure is the one nobody has to think about.

But two structures with the same Big-O can still run at wildly different speeds. Big-O is the algorithm; the metal has opinions of its own →

05Same Big-O, different metal — and the humans who saw it

Big-O deliberately throws away the constant factor (Volume 3), and that's usually the right call. But the constant is where the hardware lives, and sometimes it roars. Recall the memory hierarchy. The CPU never fetches one byte. It drags a whole cache line, 64 contiguous bytes, into fast memory at once. A contiguous array is a gift to that machine. Touch element 0 and elements 1 through 7 ride along for free, already in cache when you reach them. Pointer-chasing a linked list is the opposite. Each node lives at an unrelated address, so following a pointer can mean a fresh trip to slow RAM, a cache miss, even though the operation count is identical. Same O(n) walk, very different real speed.

Why exactly elements 1 through 7, and not 1 through 20? Do the division. A cache line is 64 bytes, and each reference in the array is 8 bytes, so one line holds 64 / 8 = 8 references. Reach for element 0 and the hardware hauls in that whole 64-byte line, which is elements 0 through 7 in one trip. The next seven reads are already sitting in fast memory, paid for. That's the free ride a contiguous layout buys you. The scattered chain gets none of it, because its next node is off at some unrelated address that the line you just loaded almost certainly doesn't cover.

One fetch grabs 64 contiguous bytes — reward the array, punish the chase array one cache line pulls all 8 1 slow fetch, then 7 free → streams linked every hop a new fetch → stalls
Fig — Two O(n) traversals, two different relationships with the cache. Contiguous data streams; scattered data stalls. This is why "same Big-O" and "same speed" are not the same claim.

An honest caveat, because this volume doesn't sell you clean stories. In pure Python the effect is muted. Summing a two-million-element list versus a linked chain of the same values clocked 46.5 ms against 59.3 ms here, only about 1.3× apart. The interpreter's own overhead swamps the cache penalty, and Python's integers are scattered heap objects either way. Drop to C or NumPy, where the values are packed inline, and the same contrast can be 10× or more. So the cache principle is real, and it governs serious systems. But its size depends on how close to the metal you are. We'll feel it at full force in the matrix and heap chapters.

None of these arrangements grew on trees. A person had to see them. That's the part worth stealing. Someone looked at a key like "session_9f3a" and thought: what if I could turn the key itself into an address, so I never search at all? That question gave us the hash table. Someone stared at a branching tree and realized it could live in a flat array with no pointers, a child's position computed by arithmetic from its parent's. That gave us the heap, the fast thing it is today. The structures are frozen insights. Learning them is really learning the moves the insights make: turn a lookup into an address, turn a shape into arithmetic, turn a walk into a jump.

↺ The thing people get backwards
Beginners think picking a structure is about the data — "I have songs, so I need a song-list." It isn't. It's about the access pattern: the operation you'll perform most. The same songs want a list if you mostly index them in order, a hash map if you mostly look one up by title, a heap if you always want the shortest one next, a graph if what matters is which song follows which in a playlist web. The data is a red herring. Ask not "what do I have?" but "what will I do to it, ten thousand times a second?" — and let that answer choose the arrangement.

If the access pattern picks the structure, then choosing well is a decision you can actually make on sight. Here's the decision — the one this whole volume is teaching you to make →

06The one skill: match the access pattern to the layout

Everything so far collapses into a single move. You have a problem, and it has a dominant operation — the thing your code does far more than anything else. Name that operation, and the structure almost picks itself, because each family exists to make one access pattern cheap. Need to keep things in order and reach them by position? That's the LINEAR family, arrays and their kin. Need to jump to a value by a key? That's KEYED, the hash map. Always need the smallest or largest next? That's HIERARCHY, the heap, a tree in an array. Data that's a grid of rows and columns? That's GRID, the matrix. Things defined by their connections? That's NETWORK, the graph. Five families, five access patterns, and the thirteen structures are all variations within them.

Try the move once, on something real. Say you're counting how often each word appears in a book, and the operation you do millions of times is this: take a word, find its running count, add one. That's a lookup by a key, the word, done over and over, so the map points you straight at KEYED, and you reach for a dict mapping each word to its count. You didn't weigh thirteen structures against each other — you just named the dominant operation and let it choose. That's the skill working exactly as advertised.

THE STDLIB TOOLBELT · choose by the verbsix tools, six access patterns — name the operation out loud and the container picks itself
from collections import deque import heapq -- name the VERB, and the container picks itself reach the k-th thinglist[k] O(1) look it up by namedict[key] O(1) avg is this one in here?x in set(…) O(1) avg add/remove at BOTH endsdeque(…) O(1) either end always want the smallestheapq.heappush(h,x) O(log n) · peek h[0] O(1) a row that never changestuple(…) immutable · hashable
listThe default, and the one every other tool must earn its way past. O(1) index, O(1) amortized append. Its slots hold 8-byte references, never your values — the boxed objects live out on the heap.
dictKey → value, O(1) average, because the key computes its own slot instead of being searched for. Keys must be hashable. Insertion-ordered since 3.7, and that order is now a language guarantee.
setThe same hash machinery asking a smaller question: is it here? No duplicates, no order. Hands you | & - for union, intersection and difference.
dequeA ring of small blocks, so append, appendleft, pop and popleft are all O(1). The price is the exact mirror of a list's: reaching into the middle, d[k], is O(n).
heapqNot a class — a handful of functions that keep an ordinary list in heap order. h[0] is always the smallest, for free; push and pop are O(log n).
tupleThe row that never changes. Immutable, therefore hashable, therefore usable as a dict key or a set member — which a list can never be.
INPUTimport timeit
n = 100_000
# drain 100,000 items from the FRONT -- same job, two tools
tl = min(timeit.repeat("while L: L.pop(0)",
         setup=f"L = list(range({n}))", number=1, repeat=5))
td = min(timeit.repeat("while D: D.popleft()",
         setup=f"from collections import deque; D = deque(range({n}))",
         number=1, repeat=5))
print(f"list.pop(0)     x{n:,}: {tl*1000:8.1f} ms")
print(f"deque.popleft() x{n:,}: {td*1000:8.1f} ms")
print(f"same job, one import apart: {tl/td:.0f}x")
OUTPUTlist.pop(0)     x100,000:   3005.3 ms
deque.popleft() x100,000:     10.3 ms
same job, one import apart: 291x
TRIPWIRES
  • x in list walks; x in set computes. The identical line of code, O(n) against O(1) — measured a few pages back at roughly eleven thousand fold on this machine.
  • list.pop(0) and list.insert(0, x) shift every remaining reference to keep the no-gaps promise. That is not a list's job at all: it is a queue's, and a queue's tool is deque. The run above is the same 100,000 items, 291× apart.
  • heapq is a min-heap only. For “the largest”, push -score or call heapq.nlargest. And h is heap-ordered, not sorted — printing it is not a ranking; only h[0] is trustworthy.
InteractiveName what you need — watch a family light up
LINEAR Array · Linked List · Stack · Queue · Deque GRID Matrix — rows × columns KEYED Hash Map — value by key HIERARCHY Binary Tree · BST · Heap · Trie NETWORK Graph · Union-Find Pick an access pattern below to see which family answers it.
This is the decision the whole volume is training — the closing decision-map chapter turns it into a full map. Right now, just feel how the operation chooses the family.
The habit to build
When you hit a new problem, resist writing code. First ask one question: what operation will this do most? Insert at the end? Look up by id? Repeatedly pull the smallest? Test membership? That single answer eliminates most of the thirteen and usually leaves one obvious winner. Choosing the structure is the design; the code after it is bookkeeping. Ninety percent of "why is this slow?" is a structure whose cheap operation isn't the one the code actually needs.

One honest edge, so you don't over-trust the map. Real problems often have two hot operations that pull toward different families. When that happens you either compose structures, like a dict of lists or a heap beside a hash map, or you accept a compromise. That tension is the interesting part of the job, not a failure of the method. The map gets you to the right neighbourhood. Judgment picks the house.

NOW NAME THE DOMINANT OPERATIONfour systems, no code — say the verb out loud, then defend the bill
The drill. For each one, write down two things before you look: the single operation this system does far more often than any other, and the container that makes exactly that operation cheap. Then defend it — name what it costs. One line each.

1. A live tournament leaderboard. Scores arrive all day from thousands of players, and the only thing ever shown on screen is who is top right now.
2. A spell-checker. For every word in a document it asks one question against a 300,000-word dictionary: is this a real word?
3. The undo history in a drawing app. Every stroke records how to reverse itself; Ctrl-Z takes back the most recent one.
4. A dashcam recording. Frames are written once, in order, then replayed — sometimes from the start, sometimes jumping straight to frame 4,812.

The check that matters: for each answer, also name the operation you made expensive in exchange. If you cannot name it, you have not chosen a structure — you have guessed one.
show the solution
import heapq

# 1 LEADERBOARD - dominant op: "who is top right now?"  -> heapq (HIERARCHY)
#   heap[0] is the smallest, so push -score and the top scorer is always slot 0.
board = []
for name, score in [("ana", 40), ("bo", 91), ("cy", 77)]:
    heapq.heappush(board, (-score, name))          # O(log n) per arrival
print("1 leader:", board[0][1], -board[0][0], " peek O(1), push O(log n)")

# 2 SPELL-CHECK - dominant op: "is this word in the dictionary?" -> set (KEYED)
words = {"cat", "hat", "the", "quick"}             # hashed: the word IS the address
print("2 in set:", "hat" in words, "| 'hta'", "hta" in words, " membership O(1)")

# 3 UNDO HISTORY - dominant op: "take back the most recent thing" -> list as a stack
undo = []
undo.append("draw line"); undo.append("fill red")  # push O(1)
print("3 undo  :", undo.pop(), "-> left", undo, " push/pop at the END, O(1)")

# 4 VIDEO FRAMES - dominant op: index by frame number, scan in order -> list (LINEAR)
frames = [f"f{i}" for i in range(5)]               # built once, never spliced
print("4 frame :", frames[3], "| in order", frames[:3], " index O(1), append O(1)")

# 1 leader: bo 91  peek O(1), push O(log n)
# 2 in set: True | 'hta' False  membership O(1)
# 3 undo  : fill red -> left ['draw line']  push/pop at the END, O(1)
# 4 frame : f3 | in order ['f0', 'f1', 'f2']  index O(1), append O(1)

# AND WHAT EACH ONE COST YOU -- the half people skip:
# 1  heap: h[0] is instant, but the full ranking is NOT in there. A heap is
#    ordered, not sorted; asking for 2nd place costs a pop, or an nlargest.
# 2  set: membership is instant, and order plus duplicates are gone forever.
#    "the 5th word in the dictionary" is a question a set cannot answer.
# 3  stack: the top is instant, and everything under it is unreachable
#    without popping. Searching an undo history is a full O(n) walk.
# 4  list: index and in-order scan are instant, and inserting a frame in the
#    MIDDLE shifts every frame after it -- O(n). Dashcams never do that.
The three questions, one last time
For every structure in Chapters 49–61, hold these: (1) how is it laid out in memory — contiguous block, scattered nodes, flat-array tree, sparse table? (2) what does each operation cost — and why does the layout force that? (3) when is it the right choice — which access pattern is it built to make cheap? Answer those three and you own the structure, not just its name.

Enough framework. Time to build. The next chapter opens the LINEAR family with the array — the single contiguous block your Python list has secretly been all along — and shows how it turns one slot of spare capacity into amortized-O(1) growth, and why "insert at the front" is the operation that quietly wrecks it. →

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

Before the thirteen structures ahead, let's meet the one idea they're all carved from — a data structure is just an arrangement of bytes in memory welded to the operations that arrangement makes cheap, so we'll build the two founding layouts by hand and watch the layout itself dictate the bill.

Two deals with memory — the array's leap vs the linked walk
The same three songs, two arrangements. Pack them side by side and any position is one address calculation away; scatter them and joined by pointers, and reaching the k-th one means walking k links. The layout, not the language, sets the cost.
The substrate — a name is a reference, not a copy
Every structure in the volume is built from one Volume 1 fact: a name holds an 8-byte address, and a list is a block of those addresses. So 'arranging data' really means arranging references — and that is exactly why aliasing surprises people.
Contract vs implementation — a stack, two ways
A stack is a promise — push, pop, peek, last-in-first-out — and says nothing about memory. Honour that promise with a contiguous list or with linked nodes; both keep every guarantee. Only when you ask a different question, like remove-from-front, does the backing send the bill soaring.
Price the operation — search by scanning vs by hashing
Ask 'is x in here?' of a list and it walks every slot; ask it of a set and it turns the key into an address and looks in one spot. Same question, radically different arrangement — and we can build that hashing trick from scratch to see the collision.
end of chapter 48 · six sections
iolinked
designed & developed by Ajai Raj
Python, from the metal up · a product of iolinked