cswork / libraryOpen practice workspace

Articles

Amortized Analysis Interview Questions: Dynamic Arrays, Queues, and Misleading Worst Cases

Practice amortized analysis interviews with dynamic arrays, two-stack queues, potential proofs, resize thrashing, and worst-case distinctions.

Amortized analysis interview questions test whether you can reason about a sequence of operations without hiding an expensive individual call. A dynamic-array append can cost linear time when it reallocates. A two-stack queue dequeue can move every pending item. Both can still have constant amortized cost because the expensive events cannot happen on every operation.

This guide builds the proof tools behind that prompt: aggregate counting, accounting charges, potential functions, resize policies, adversarial sequences, and the difference between amortized, average-case, and worst-case bounds.

Amortized Analysis Interview Questions: Quick Answer

Amortized analysis bounds the total cost of any valid operation sequence, then divides that bound across the operations. It does not assume random input. If every sequence of (m) operations costs at most (O(m)), the amortized cost is (O(1)), even when a particular operation costs (O(n)).

Use the aggregate method when each object participates in only a constant number of expensive events. Use accounting when cheap operations can prepay later work. Use a potential function when the data structure's stored work rises and falls across several operation types. Always state the actual per-operation worst case separately.

Separate four claims interview answers often mix together

ClaimMeaningExample
Worst-case per operationMaximum cost of one callA resize append copies (n) elements: (O(n))
AmortizedWorst total over a sequence, averaged per callDoubling-array append: (O(1)) amortized
Average-caseExpected cost under an input distributionHash lookup under stated hashing assumptions
Expected amortizedBoth randomness and sequences matterA randomized structure with an expected sequence bound

Saying “resizes are rare” is intuition, not a proof. Saying “usually (O(1))” sounds like average-case probability. A defensible answer gives a sequence cost, a charge invariant, or a telescoping potential equation.

The evidence boundary matters here. University texts and lecture notes establish the mathematical methods. Recommendations about how to narrate the proof are editorial inference.

Prove dynamic-array growth by counting copies

Assume capacity starts at 1 and doubles whenever an append finds the array full. Charge one unit to write the new element. When capacities grow through 1,2,4,…1,2,4,\ldots, reallocations copy

1 + 2 + 4 + ... + 2^j < 2n

elements before the array reaches size (n). The (n) direct writes plus fewer than (2n) copies cost less than (3n). Therefore (n) appends cost (O(n)), or (O(1)) amortized each. One append still has (O(n)) worst-case latency.

This proof depends on geometric growth. Growing capacity by one makes the copy total 1+2+⋯+(n−1)=Θ(n2)1+2+\cdots+(n-1)=\Theta(n^2), so append becomes Θ(n)\Theta(n) amortized. A factor (g>1) preserves constant amortized append, while smaller factors trade less unused capacity for more frequent copying.

The candidate-reported Implement flat_map and design wealth redistribution uses sorted dynamic-array storage. Its insertion may also shift elements to preserve order. Doubling proves that reallocation is amortized constant; it does not erase the linear shifting cost of inserting near the front. Analyze each source of work separately.

Use accounting to make the resize invariant explicit

One accounting proof charges three units per append: one pays for the immediate write and two credits are saved to help copy elements at the next doubling. The exact credit placement can vary, but the invariant must show that a full array has accumulated enough credit to fund its next copy.

Credits are analysis devices, not runtime objects. They may never go negative under a valid sequence. If your charging scheme borrows from future operations, it has not proved the bound. Walk through capacities 1, 2, 4, and 8 and show the balance immediately before each resize.

Deletions expose resize-policy mistakes

An array that doubles when full and halves whenever size drops below one-half can thrash. Start near the boundary, delete once to shrink, append once to grow, and repeat. Each operation can copy a linear number of elements.

Hysteresis fixes the policy: grow at load 1, but shrink only when load falls to one-quarter, usually halving capacity. After a shrink, the table is about half full, so many updates must occur before either boundary is reached again. The gap between thresholds pays for the next resize.

A potential proof can encode that stored distance from a resize boundary. The exact piecewise function depends on the policy. State three obligations: the initial potential is zero, potential never becomes negative, and actual cost plus the change in potential is bounded. A remembered formula with the wrong shrink threshold is worse than a fresh aggregate argument.

Prove the two-stack queue by following each element

Maintain in_stack for enqueue and out_stack for dequeue. Push new items onto in_stack. When a dequeue or peek finds out_stack empty, move every item from in_stack to out_stack; the reversal puts the oldest item on top.

class TwoStackQueue:
    def __init__(self):
        self.ins, self.outs = [], []

    def push(self, x):
        self.ins.append(x)

    def _prepare(self):
        if not self.outs:
            while self.ins:
                self.outs.append(self.ins.pop())

    def pop(self):
        self._prepare()
        return self.outs.pop()

An element is pushed onto in_stack once, popped from it once, pushed onto out_stack once, and popped from that stack once. Across (m) queue operations, there are at most a constant number of stack operations per element, so total time is (O(m)). A transfer-triggering pop remains (O(n)) worst-case.

For a potential proof, choose (\Phi=2|in_stack|). An enqueue performs one actual push and raises potential by two, so its amortized charge is three. A transfer of (k) elements plus the final pop has actual cost about (2k+1), while potential falls by (2k), leaving constant amortized cost. The potential telescopes across the sequence.

Do not let amortized cost promise latency

Amortized (O(1)) is a throughput statement over a sequence. It does not promise that each operation finishes quickly. A UI frame, trading path, or real-time controller may care about the (O(n)) resize pause even if long-run throughput is linear.

If the contract requires bounded per-call latency, discuss incremental migration, fixed-capacity circular buffers, chunked arrays, or a real-time queue. These designs change invariants and constants. The candidate-reported Design a queue and analyze tradeoffs makes that distinction concrete: a fixed circular buffer avoids transfer and resize pauses but has a capacity/overflow contract.

Diagnose misleading worst-case arguments

ClaimAdversarial checkCorrect conclusion
“One append is (O(n)), so all appends are (O(n))”Sum copy sizes at powers of twoIndividual worst case (O(n)); amortized (O(1))
“Resizing is always amortized (O(1))”Grow capacity by oneTotal copying is quadratic
“Grow and shrink at 50%”Alternate around the thresholdRepeated linear copies cause thrashing
“Two stacks make every dequeue (O(1))”Dequeue after (n) enqueuesOne dequeue transfers (n) items
“Amortized means expected”Choose a deterministic adversarial sequenceNo probability distribution is required
“Potential may go negative temporarily”Stop the sequence at that stateThe claimed upper bound can fail

Also watch for hidden work. Python list slicing, copying an iterator snapshot, rebuilding a hash table, or preserving sorted order may dominate the operation you proved. The candidate-reported Explain hash map internals and edge cases requires separate reasoning about collisions, resizing, and deletion semantics. An average lookup claim does not prove a resize bound.

Build a proof from the operation sequence

In an interview, write a small ledger with operation, actual cost, state before/after, potential change, and amortized cost. Test a quiet sequence and a boundary-heavy one. For arrays, append through two resizes, then alternate operations near shrink thresholds. For queues, enqueue several items, trigger one transfer, enqueue more while out_stack remains nonempty, and drain everything.

The potential identity is

sum(amortized cost) = sum(actual cost) + Φ(final) - Φ(initial)

If (\Phi(initial)=0) and Φ≥0\Phi\ge0, total actual cost is at most total amortized cost. This telescoping step is the proof; choosing a useful potential is the design work.

Compare the three proof methods on the same example

The methods prove the same kind of sequence bound, but expose different reasoning. For a doubling array, aggregate analysis sums the geometric copy series. Accounting charges each append extra and saves credit. Potential treats a partly filled array as stored ability to pay for its next resize.

Do not present three full proofs unless asked. Start with the shortest rigorous one, then explain how another method represents the same work. Aggregate counting is usually clearest for a two-stack queue because “each element moves once” is visible. Potential is useful when pushes, pops, growth, and shrinkage interact and an object-level story becomes awkward.

Test a proposed potential mechanically. Compute it before and after a cheap operation, a resize, and a boundary reversal. If actual cost plus potential change remains large on the expensive step, the function has not stored enough work. If potential becomes negative, stopping the sequence there breaks the upper-bound argument.

State the cost model before calculating

An amortized proof is only as precise as its unit costs. A classroom array model may charge one unit per element written. Real code can add allocation, destructor, cache, and exception costs. State the intended model and keep the proof inside it. Then mention implementation consequences without claiming the asymptotic proof measures wall-clock latency.

The same discipline applies to persistent structures. A snapshot iterator that copies the current array has linear creation cost even if every later next() is constant. Charging that copy to future iteration works only when the API guarantees those iterations occur; otherwise a caller can create snapshots and discard them, leaving the work unpaid.

Handle deamortization as a contract change

When an interviewer asks for worst-case constant time, repeating the amortized proof does not answer the follow-up. One strategy spreads a resize across later operations: allocate the new table, migrate a bounded number of old entries per call, and route lookups across both tables until migration finishes. The implementation must prevent duplicate moves, preserve updates to keys still in the old table, and finish migration before another resize.

This improves the per-call bound under a carefully defined model, but temporarily stores two tables and complicates every operation. A fixed circular buffer gives a simpler worst-case bound when rejecting or blocking on overflow is acceptable. Choose from the latency and capacity contract.

Final takeaway

Name the expensive operation, then show why it cannot recur too often. Count every copied or transferred element, state the sequence bound, and preserve the difference between per-call worst case and amortized cost. If deletions or latency requirements enter the prompt, re-check the policy instead of repeating the doubling-array slogan.

Continue with the five linked Coding & Algorithms questions. For each one, write both a counterexample to the naive claim and a proof ledger that survives it.

Sources and Further Reading

Technical sources support the mathematical claims.