Exercises — Array — static, dynamic; cache locality; amortized O(1) append
The three tools we lean on:
- the address formula (base address plus index times element size),
- the geometric-series total-copy bound (derived in full in Solution 3.1),
- the cache-line model elements per fetch.
Before we start, one shared picture of the machinery we are testing:

How to read this figure. Each cyan box is one array slot, labelled arr[0], arr[1], … along the bottom. Under each box, in amber, is its actual memory address computed by : 2000, 2004, 2008, … The amber double-arrow across the top marks the constant gap bytes between neighbours. The single idea the picture carries: because the gap never changes, the address of any box is one multiply plus one add — you jump straight to it, never walking the boxes in between.
Level 1 — Recognition
Goal: can you name the fact and read it off directly?
Problem 1.1
An int array (each element bytes) begins at base address . Using only the address formula, where does element arr[7] live?
Recall Solution 1.1
WHAT we do: apply with , , . WHY this tool: the address of any element is computed, not searched — that is the whole point of contiguity. Notice we never touched elements 0–6. Answer: 2028.
Problem 1.2
A dynamic array reports size = 6, capacity = 8. Answer each:
(a) How many elements are actually stored?
(b) How many slots are allocated?
(c) Will the next append trigger a resize?
Recall Solution 1.2
WHAT/WHY: size = elements you put in; capacity = slots allocated; a resize fires only when size == capacity.
- (a)
size = 6→ 6 elements stored. - (b)
capacity = 8→ 8 slots allocated. - (c) After one append
sizebecomes 7, still . So no resize — it's a cheap O(1) write into slot index 6.
Problem 1.3
Two data structures store the same 1,000,000 integers. Both have scan cost in Big-O Notation. One runs ~7× faster in wall-clock time. Which one, and what single property causes it?
Recall Solution 1.3
Answer: the array. The property is contiguity (contiguous memory with fixed element size).
WHY: the CPU loads a whole cache line at once; array neighbours ride in together, so most reads are cache hits. A Linked List scatters nodes, so each next is likely a cache miss. Same Big-O, very different constant.
Level 2 — Application
Goal: plug numbers into the derived formulas correctly.
Problem 2.1
A double array uses bytes per element, base . Find the addresses of arr[0], arr[3], and arr[10]. What is the gap between consecutive elements?
Recall Solution 2.1
Gap: bytes — constant and predictable, which is exactly what lets the hardware prefetcher guess ahead. Answers: 4096, 4120, 4176; gap = 8.
Problem 2.2
A cache line is bytes; elements are short at bytes each. (a) How many elements load per fetch? (b) Scanning 800,000 shorts, roughly how many slow RAM fetches?
Recall Solution 2.2
WHY this tool: one memory fetch loads contiguous elements; scanning (recall: = how many elements we touch) costs fetches.
- (a) elements per fetch.
- (b) fetches (versus 800,000 if scattered).
Problem 2.3
A doubling dynamic array starts with capacity = 1. You call append exactly 5 times (so here ). Total copy work? Verify it is below the bound.
Recall Solution 2.3
Resizes fire when size == capacity, at sizes (each copies the current elements before doubling):
- append 1 → cap 1, write. copies 0
- append 2 → size(1)==cap(1): copy 1, cap→2
- append 3 → size(2)==cap(2): copy 2, cap→4
- append 4 → cap 4, write. copies 0
- append 5 → size(4)==cap(4): copy 4, cap→8 Bound check: , and . ✓ Total copies = 7.
Level 3 — Analysis
Goal: explain the mechanism, not just compute.
Problem 3.1
Prove that with doubling growth the total copy cost over appends is , starting from an empty array of capacity = 1. Show the geometric-series step explicitly.
Recall Solution 3.1
WHAT: sum the copies at every resize. Resizes occur at sizes where .
WHY these numbers: a resize copies the current count, then doubles capacity, so the counts copied are exactly the powers of two.
Deriving the geometric-series identity from scratch (do not just cite it — earn it). Call the sum :
Now multiply every term by the ratio :
WHY multiply by 2? Because doubling shifts the whole list up by one term, so almost everything cancels when we subtract. Line up and subtract from :
Every middle term () appears in both sums and cancels; only the new top and the old bottom survive. So:
Now the cheap writes. In our cost model each unit of work = "one element touched." Every one of the appends performs exactly one write of the new element into its slot (old[size] = x) — that is 1 unit each, so appends contribute write-units. These are separate from copy-units. Total work .
Divide by appends → amortized. ∎ (This is the amortized argument.) The figure below plots exactly this — watch the cyan copy-total stay pinned under the amber line.

How to read this figure. The horizontal axis is , the number of appends so far; the vertical axis is the running total of elements copied. The cyan staircase jumps upward only at the rare resize points () and stays flat in between — visual proof that most appends copy nothing. The dashed amber line is the ceiling . The cyan staircase never crosses it: that gap is the -amortized guarantee made visible.
Problem 3.2
Your teammate grows the array by +10 slots each time it fills (additive, not multiplicative). Over appends, derive the total copy cost and its amortized per-append cost. Why is this bad?
Recall Solution 3.2
WHY additive fails: with a fixed step , resizes happen at sizes — that is every 10 appends, and the count copied keeps growing linearly. There are about resizes, and the -th resize copies elements. The copies form an arithmetic series (not geometric — the terms grow by a constant difference , not a constant ratio): Deriving the arithmetic-sum from scratch (call ). Write the sum forwards and backwards and add termwise: Each of the paired columns sums to , so twice the sum is : WHY the pairing trick? Pairing the largest with the smallest makes every column identical, turning a sum into one multiplication — the classic Gauss argument. Substitute : (the is negligible for large ). That is total → dividing by appends gives amortized per append. Terrible. The lesson: you must grow by a multiplicative factor (×2, ×1.5). The gaps between resizes must grow, not stay constant.
Problem 3.3
Two arrays of 1,000,000 ints (), cache line . Array A is scanned front-to-back. Array B is scanned by jumping every 16th element (stride 16). Compare the number of cache-line fetches. Explain the result.
Recall Solution 3.3
Elements per line: .
- Array A (stride 1): consecutive elements share lines. Fetches .
- Array B (stride 16): each accessed element lands in a different line (they're exactly one line apart), so you touch a fresh line every read. Accesses made , and each costs its own fetch → fetches for only elements used. The point: A gets 16 elements per fetch (locality wins); B gets ~1 useful element per fetch (locality destroyed by the stride). Same Big-O, but B wastes every cache line — a classic "cache-unfriendly access pattern."
Level 4 — Synthesis
Goal: combine growth, copying, and memory into one design decision.
Problem 4.1
A library grows its array by ×1.5 instead of ×2 (start capacity 1, rounding up). (a) Is append still amortized ? (b) What is the trade-off versus ×2 in wasted memory?
Recall Solution 4.1
(a) Yes. Any growth factor gives a geometric series with ratio . Total copies: (This closed form is the same "multiply-by- and subtract" derivation as Solution 3.1, now with ratio instead of : , so .) With : constant , so → amortized. (For the constant is .) (b) Trade-off: ×1.5 wastes less memory (worst-case ~50% over-allocation vs ~100% for ×2) but copies more often (bigger constant, 3 vs 2). ×1.5 = memory-friendly; ×2 = fewer copies. Both are amortized — you're choosing the constant.
Problem 4.2
Design the append(x) operation in pseudocode and annotate the cost of each branch. Then state the worst-case single-append cost and the amortized cost, and say why they differ.
Recall Solution 4.2
append(x):
if size == capacity: # array is full
newcap = max(1, 2 * capacity) # geometric growth
B_new = allocate(newcap * s) # cost model below
copy old[0..size-1] -> B_new # O(n) <-- the expensive part
free(old); B = B_new; capacity = newcap
old[size] = x # O(1) write
size = size + 1
- Cheap branch (
size < capacity): just the write + increment → O(1). - Expensive branch (
size == capacity): copy allsizeelements → O(n). - The
allocatecost model. We treatallocate(newcap * s)as O(1). WHY is that fair? A modern allocator reserves a block by handing back a pointer and recording the size in its bookkeeping — it does not touch every byte of the block (the memory is left uninitialised until you write it). So the reservation itself is a constant-time bookkeeping step; the genuine per-element work happens in the copy line, which we already charge as . (If instead you demanded zero-initialised memory, the zeroing would be — but that is still absorbed into the same geometric total, so the amortized result is unchanged.) - Worst-case single append: (the resize copy).
- Amortized append: (Problem 3.1: total copies over appends). Why they differ: the resize is rare — its cost, spread across the many cheap appends that follow, averages to a constant. Worst-case looks at one call; amortized looks at the whole sequence.
Problem 4.3
A stack is built on a dynamic array: push = append, pop = size--. You push items then pop them all. (a) Total cost of the pushes? (b) Total cost of the pops? (c) Overall amortized cost per operation?
Recall Solution 4.3
- (a) pushes with doubling: total (copies plus writes).
- (b)
popjust decrementssize(no shrink assumed): each is , total . - (c) Total work over operations , so per operation amortized. That's why an array-backed stack is the standard, fast choice.
Level 5 — Mastery
Goal: prove and generalize.
Problem 5.1
Prove: for any constant growth factor , doubling-style append is amortized, and state the exact amortized copy-constant as a function of . Then find the that minimizes copies-per-element as and interpret.
Recall Solution 5.1
Resizes copy with . Total copies use the geometric closed form derived in Solution 3.1 (multiply-by-, subtract): using . So amortized copies per append: This is a finite constant for every → amortized. ∎ Behaviour of : it decreases as grows: , , , and as . Interpretation: bigger → fewer copies per element (approaching 1, the theoretical floor) but worse wasted memory (worst-case over-allocation ). So trades time-constant against space-constant. Real libraries pick as the sweet spot.
Problem 5.2
You are told a mystery container has worst-case (not amortized) append and index access. Can it be a plain dynamic array? Argue rigorously, and name what you'd combine to approach worst-case append.
Recall Solution 5.2
No — a plain dynamic array cannot have worst-case append. Proof by the pigeonhole of contiguity: to keep elements contiguous (which is required for index access via ), a full array must move to a larger contiguous block, and some append must pay the copy. If that copy stayed , you could only ever copy a constant number of elements, contradicting that the block holds of them. Hence at least one append is → worst-case is not . How to approach it: use incremental / geometric resizing with copy-spreading — during the "next" resize, copy a fixed few old elements per append (keep both blocks alive, migrate lazily). This bounds each append's work by a constant while preserving contiguous access on the new block. (This is the classic "amortized→worst-case deamortization" trick; contrast with a Hash Table or Linked List, which give different worst-case profiles.)
Problem 5.3
A dynamic array of int () currently has capacity = 16 sitting in cache lines of bytes. (a) How many cache lines does the entire allocated block occupy? (b) After a doubling resize to capacity = 32, how many cache lines does the new block need? (c) During the resize copy, roughly how many line-fetches to read all 16 old elements sequentially?
Recall Solution 5.3
- (a) Block bytes . Lines line.
- (b) New block bytes . Lines lines.
- (c) Reading 16 contiguous ints sequentially: line-fetch to stream all of them. Contiguity makes even the copy step itself cache-efficient — the resize is in element-work but touches memory minimally.
Recall One-glance recap of every formula used
::: address of element i (base + index × element size) Elements loaded per cache fetch ::: (line bytes ÷ element bytes) Total copies, doubling, n appends ::: Amortized copy-constant for factor g ::: (2 for ×2, 3 for ×1.5, →1 as g→∞) Additive (+c) growth total copies ::: arithmetic series Worst-case single append (dynamic array) ::: (the resize copy)