Worked examples — Array — static, dynamic; cache locality; amortized O(1) append
This page is the stress-test for the parent Array note. There we derived the three big ideas (address formula, geometric growth, cache locality). Here we use them on every kind of input the topic can throw at you — small caps, big caps, a growth factor that isn't 2, an empty array, a resize that happens on the very first append, a real-world timing question, and an exam trap.
Read each [!example] statement, make a forecast before scrolling, then check your instinct against the steps.
The scenario matrix
First, the one symbol the whole page turns on, defined before the table uses it:
Now every problem about dynamic arrays lands in one of these cells. We will hit all of them.
| # | Case class | What makes it special | Covered by |
|---|---|---|---|
| A | Cheap append (size < capacity) |
just a write, no copy | Ex 1 |
| B | Expensive append (size == capacity) |
triggers a resize + copy | Ex 1, Ex 2 |
| C | Doubling growth (growth factor ) | the classic amortized-O(1) case | Ex 2 |
| D | Growth factor (e.g. ) | still O(1) amortized — must re-derive | Ex 3 |
| E | Degenerate: empty array / first append | capacity 0 → what does "double 0" mean? | Ex 4 |
| F | Additive growth (+k) — the trap | O(n²) total, O(n) amortized | Ex 5 |
| G | Address arithmetic (static-array flavour) | pure B + i·s, sign/edge indices |
Ex 6 |
| H | Real-world timing (cache locality) | array vs linked list wall-clock | Ex 7 |
| I | Exam twist (which grows slower: total copies) | compare two growth factors head-to-head | Ex 8 |
Example 1 — Cheap vs expensive appends (cells A, B)

Figure description (Ex 1). A bar chart with one bar per append: append 10, append 20, append 30, append 40. Each bar has an orange base of height 1 = the single write every append does. The third bar (append 30) additionally carries a violet stack of height 2 = the two elements copied during its resize, so it stands three units tall while all others stay one unit tall and are labelled "cheap." A magenta arrow points at that tall bar reading "full: size==cap triggers resize." The single tall violet bar is the one expensive append; every other bar is a flat cheap write — that shape is the meaning of "amortized": rare spikes averaged across many flat writes.
Example 2 — Total copy cost with doubling (cell C)
Example 3 — Growth factor ≠ 2 (cell D)
Why it feels right: less wasted memory. The fix: the amortized-copy constant is , which blows up as (at it's — that's the +1 trap of Ex 5). It's a trade-off: small = less memory, more copying; big = more memory, less copying.
Example 4 — The degenerate empty array (cell E)
A brand-new vector has , . You call append(x). But — doubling zero gives zero! How does the append ever get room?
Forecast: what capacity does the array jump to on the very first append? Guess a number.
size(0) == cap(0)→ the array is "full" (0 of 0 used), so a resize must fire. Why this step? The full-check issize == cap, and is true.- New capacity — useless. So every real implementation special-cases this: if cap is 0, jump to a fixed small starting capacity (commonly 1 or 4). Why this step? Multiplicative growth can't escape zero; you need an additive seed exactly once.
- With seed : cap , copy 0 old elements (there are none), write
x.size→1. Why this step? We pick the smallest useful seed, , because it makes every later doubling () match the clean powers-of-two derivation the parent used, and it wastes the least memory for the many arrays that only ever hold a handful of items. (Libraries that expect larger arrays seed at 4 to skip the tiny first few resizes — a memory-vs-copies choice, not a correctness one.)
Verify: copies done (empty source). After the append: , . This is exactly the "start cap 1" assumption the parent's derivation used — now you know where that 1 comes from. ✓
Edge check — appending into an empty array is still O(1) amortized: the one-time seed is a constant cost paid once, so it doesn't change the average.
Example 5 — The additive-growth trap (cell F)
each time, appends A misguided vector grows capacity by a fixed +2 whenever full (start cap 2). Append 8 elements. Total copies? Amortized cost?
Forecast: the parent warned additive growth is . Do you think total copies here is closer to 8, or closer to 30? Guess.
- Caps visited: (each resize adds 2). Resizes fire at sizes (the append that fills each cap). Why this step? +2 growth means a resize almost every couple of appends — far more often than doubling.
- Copies at each resize = current size = .
Why this step? Just like the doubling case, a resize must move every element that already exists into the new block, and that count is exactly
sizeat the moment of resize (which here equals the old cap ). The rule "copies = current size" is universal; only how often it fires changes with the growth policy. - Total copies . Why this step? This is an arithmetic series , not geometric — it grows like , not .
Verify — scale it up. For general additive growth by , resizes copy up to , totalling . Divide by : amortized — quadratic total, linear per append, the disaster the parent note flagged. With our formula predicts ; the exact small- count was (the approximation ignores rounding), same quadratic shape. ✓
Contrast: doubling 8 elements (start cap 2) copies only . Additive +2 copies — and the gap widens without bound as grows. That is why real dynamic arrays multiply.
Example 6 — Address arithmetic, edge indices (cell G)
double d[100] (8 bytes each), base
Give the address of d[0], d[99], and explain why d[100] is a bug even though the formula produces a number.
Forecast: guess addr(99) before computing.
- Formula from the parent: with , . Why this step? Element starts bytes after ; by induction, elements past base is bytes.
- — the base itself. Why this step? Index is the first element, so it sits at the very start of the block with zero offset: . This is why arrays count from 0 — the index literally is "how many elements to skip," and you skip none to reach the first.
- . Why this step? To reach the 100th element (index 99) you skip the 99 elements before it, each bytes wide, so the offset is bytes added to the base.
- — a valid-looking address, but it's one past the last element (valid indices are ). Reading it is undefined behaviour / out-of-bounds. Why this step? The math never "knows" the array's length; you enforce . This is the classic off-by-one.
Verify: the block spans up to . So the last byte of the array is 4799; address 4800 is outside. ✓ Units check: bytes + (dimensionless index × bytes) = bytes. ✓
Example 7 — Real-world timing: array vs linked list (cell H)
You sum a contiguous array of 1,000,000 four-byte ints. Cache line bytes. Estimate the slow-memory fetches, and compare to a Linked List holding the same numbers.
Forecast: guess the ratio of linked-list fetches to array fetches.
- Elements per cache line . Why this step? One fetch loads a whole 64-byte line; that's neighbouring ints at once (see CPU Cache and Memory Hierarchy).
- Array fetches . Why this step? Because is tiny and predictable, one fetch serves the next 15 reads for free — you only pay a slow RAM trip once per 16 elements.
- Linked list: nodes are scattered, each
nextis a fresh line → up to fetches. Why this step? A linked-list node ismalloc-ed independently, so consecutive nodes can sit anywhere in memory — the address gap between one node and the next is unpredictable and usually larger than a 64-byte line. So the neighbours that the cache line drags in are not the nodes you'll visit next; eachnextpointer jumps to an address the cache has never seen → a fresh miss per node.
Verify: ratio fewer memory trips for the array — even though both are in Big-O Notation. Big-O hides the constant; the cache line is that constant. This is why "same complexity" ≠ "same speed." In practice the array wins ~5–10× wall-clock (not the full 16×, because prefetching, TLB effects and partial locality soften the linked list's worst case). ✓
Example 8 — Exam twist: which factor copies less over appends? (cell I)
vs : which does fewer total copies as ? A quiz asks: "Doubling () vs quadrupling () — which has the smaller amortized copy constant?" No tracing allowed; use the closed form.
Forecast: more aggressive growth (×4) resizes less often. So does it copy less? Guess yes/no.
- From Ex 3, amortized copy constant . Why this step? Total copies , so per-append copies .
- : . : . Why this step? We just substitute each concrete growth factor into the general constant : for the denominator is , giving ; for the denominator is , giving . Comparing the two numbers is what the exam actually asks.
- So copies fewer elements per append ( vs ). Why this step? Bigger jumps mean resizes are rarer, so less total moving — the intuition holds.
Verify — the catch. is decreasing in , so higher = fewer copies. But memory over-allocation is up to = waste for vs for . So trades memory for speed. The exam-correct answer: does fewer copies (), at the cost of memory. Both are still amortized because is finite for every . ✓
Recall Which cells did we cover?
A,B (Ex1) ::: cheap + expensive appends C (Ex2) ::: doubling total copies = 15 for n=16 D (Ex3) ::: g=1.5 still O(1), constant 3 E (Ex4) ::: empty array seeds cap 0→1 F (Ex5) ::: additive +2 gives O(n²) total (12 copies for n=8) G (Ex6) ::: addr(99)=4792, addr(100) is out-of-bounds H (Ex7) ::: array 62,500 fetches vs list ~1,000,000 (16× fewer) I (Ex8) ::: g=4 constant 4/3 < g=2 constant 2
"SEED, then MULTIPLY" — an empty array needs one additive seed (0→1); after that always multiply by . The amortized copy cost is : finite for any , infinite at (the +k trap).
Connections
This page leans on Amortized Analysis and Geometric Series for the copy-cost sums, CPU Cache and Memory Hierarchy for Ex 7, and contrasts arrays with Linked List. The append pattern underlies Stack (Data Structure) (push = append) and the buckets of a Hash Table. Timing claims are stated in Big-O Notation, which is the language for saying "both are yet one is 16× faster in practice."