5.2.19 · D2C++ Programming

Visual walkthrough — STL containers — vector, list, deque, array, set, multiset, map, multimap, unordered_map, unordered_set

2,119 words10 min readBack to topic

We are going to earn the parent note's central formula: one picture at a time. If any of those symbols look scary now — good. By Step 8 they will feel obvious.


Step 1 — What a vector actually is in memory

WHAT. A std::vector is a wrapper around three numbers: a pointer to a block of memory, a size (how many slots are used), and a capacity (how many slots the block can hold before it is full).

WHY. You cannot reason about the cost of growing something until you can see what "full" means. The whole argument hinges on the gap between size and capacity.

PICTURE. Below, the grey slots are reserved but empty (capacity not yet used); the cyan slots are occupied (the size). Notice: push_back just writes into the next grey slot and bumps the size by one. That is a single write — as long as a grey slot exists.

Figure — STL containers — vector, list, deque, array, set, multiset, map, multimap, unordered_map, unordered_set

Step 2 — The cheap case and the expensive case

WHAT. There are exactly two things that can happen when you call push_back:

  • Cheap (): write into a free slot, . Constant work.
  • Expensive (): no room — allocate a bigger block, copy all old elements over, free the old block, then write. This copy is work.

WHY. If every push were expensive, a vector would be a terrible container. The magic is that the expensive case is rare. To measure "how rare", we first have to fix the rule for how much bigger the new block is.

PICTURE. Left: a push landing in a free slot (one arrow, one write). Right: a full block forcing a reallocation — every existing element gets carried to a new, larger home.

Figure — STL containers — vector, list, deque, array, set, multiset, map, multimap, unordered_map, unordered_set

Step 3 — The growth rule: why doubling?

WHAT. When full, the vector allocates a block of capacity (the typical libstdc++/MSVC choice). It doubles.

WHY doubling and not "+1 each time"? This is the key design decision, so let us justify it, not assume it. Suppose instead we grew by a fixed +1 every time it filled. Then every single push would be a "full" push, copying all elements: total copies — quadratic! Doubling instead makes the full-events get exponentially rarer as the vector grows, which is exactly what turns into .

PICTURE. Two timelines of "when does a reallocation fire?" The +1 strategy fires constantly; the doubling strategy fires at — the gaps double each time.

Figure — STL containers — vector, list, deque, array, set, multiset, map, multimap, unordered_map, unordered_set

Step 4 — List every expensive event

WHAT. Start from an empty vector and push elements. The reallocations (copies) happen exactly when size hits , where is the largest power of two that is .

WHY. To add up the total cost, we must first enumerate every expensive moment and how much each one costs. A reallocation at capacity copies exactly elements (it carries the whole existing block).

PICTURE. A number line of pushes . Amber spikes mark the reallocations; each spike is labelled with its copy cost (). Everything between spikes is a flat run of cheap pushes.

Figure — STL containers — vector, list, deque, array, set, multiset, map, multimap, unordered_map, unordered_set

Step 5 — Sum the copy work (the geometric series)

WHAT. Total copy work across all reallocations:

WHY this tool — the geometric sum? Each term is double the one before (constant ratio ). A sum where each term multiplies by a fixed number is a geometric series, and it has a famously clean closed form. We reach for it precisely because "each reallocation copies twice as much as the last" is a doubling pattern — the exact shape geometric series were invented to collapse.

PICTURE. The classic proof-without-words: stack bars of heights . The whole stack is one short of the next bar . That single missing unit is the entire content of the closed form.

Figure — STL containers — vector, list, deque, array, set, multiset, map, multimap, unordered_map, unordered_set

Step 6 — Bound it by

WHAT. Replace the abstract with something in terms of :

WHY. We defined so that (Step 4). Multiply both sides by 2: . And dropping the only makes the left side larger, so the strict inequality holds. This converts "sum over reallocations" into a plain bound the reader can feel: all the copying together is less than twice .

PICTURE. Two bars side by side: the total copy work , and the ceiling . never touches the ceiling, no matter how big grows.

Figure — STL containers — vector, list, deque, array, set, multiset, map, multimap, unordered_map, unordered_set

Step 7 — Divide out: the amortised cost

WHAT. Amortised analysis asks: "spread the total cost evenly across all operations, what does each one cost on average?"

WHY divide? Amortised does not mean "every push is cheap" — one push (a reallocation) really is . It means the bill split across the whole sequence is constant. Dividing the total by the count is exactly that split.

PICTURE. A jagged red line of the true per-push cost (mostly at height 1, with tall spikes at powers of two) and a flat amber line at height — the average. The spikes poke above the flat line but the calm valleys pull the average back down.

Figure — STL containers — vector, list, deque, array, set, multiset, map, multimap, unordered_map, unordered_set

Step 8 — The degenerate and edge cases

WHAT & WHY. A derivation is not finished until every corner is covered.

  1. (empty): no pushes, no reallocations, . The formula gives which is an empty/undefined sum — we simply treat it as . No contradiction: zero work for zero pushes.
  2. (single push): , one reallocation copying element... actually the first push into a fresh empty vector allocates but copies nothing (there is nothing to copy). So real implementations do even better than the bound — the bound is an upper limit, never violated.
  3. reserve(n) first: if you tell the vector its final size up front, it allocates once and every push is the cheap branch — zero reallocations. This is the fix the parent note's mistake box points at.
  4. Growth factor (e.g. in some libraries): the ratio still gives a geometric series , still total, still amortised. Any constant factor works — only "+1 growth" (ratio effectively ) breaks it back to .

PICTURE. Four mini-panels: empty, single push, pre-reserved (flat, no spikes), and growth (spikes closer together but still exponentially sparse).

Figure — STL containers — vector, list, deque, array, set, multiset, map, multimap, unordered_map, unordered_set

The one-picture summary

Everything above compressed: the per-push cost curve (spikes at powers of two), the running total rising along a line bounded by , and the flat amortised average — all on one blueprint.

Figure — STL containers — vector, list, deque, array, set, multiset, map, multimap, unordered_map, unordered_set
Recall Feynman retelling — say it like you'd explain to a friend

A vector is a box with spare room. Adding a thing is usually free — you just drop it in a spare slot. But when the box fills up, you have to buy a box twice as big and carry everything over. That carrying is slow. Here's the trick: because you double every time, the box fills up half as often as it grew last time — the slow moves get rarer and rarer. If you add up ALL the carrying you ever do, it's less than twice the number of things you stored. Split that bill across every "add", and each add costs a tiny constant on average. That's what "amortised " means: rare expensive moves, paid for by a crowd of cheap ones. And if you knew the size all along, just reserve it — then you never carry anything at all.

Recall Quick self-test

Why does growing by "+1" each time give total? ::: Every push would be a "full" push copying all elements, so total . What is and what does it mean here? ::: — the last reallocation before reaching 100 elements copied items. A single push_back worst case is what complexity? ::: — when it triggers a reallocation and copies the whole array. The closed form of ? ::: . How do you avoid all reallocations when the size is known? ::: Call reserve(n) first.

Prerequisites revisited: Amortised analysis, Big-O notation, and the memory-layout intuition from Cache locality all feed this result. Back to the parent: STL containers overview.