Data Structures & Algorithms / Optimization proofs and maintained aggregates / Lesson 18
Amortized costs and algorithm selection
Bound operation sequences without confusing amortization, randomness, worst-case latency, or measured implementation behavior.
On this page
Lesson goals
- One expensive operation can fit a cheap total sequence
- Model doubling explicitly instead of guessing Python capacity
- Charge each queue element for one transfer
One expensive operation can fit a cheap total sequence
Amortized analysis bounds total cost across a sequence of operations, including occasional expensive ones. It does not average over random input unless randomness is separately part of the model. A dynamic array that doubles capacity can occasionally copy every stored element, yet the total copied across many appends is geometrically bounded. This does not promise every append has constant worst-case latency. If a real-time task needs a strict per-operation deadline, an amortized guarantee alone may be insufficient. Name the sequence, starting state, charged work, and allowed operations before presenting an average-looking number.
Model doubling explicitly instead of guessing Python capacity
The first program counts writes and copied elements in an abstract doubling array. It is not an inspection of CPython’s actual allocation policy. Starting at capacity zero, the first allocation creates one slot, then later full capacities double. The copies before reaching n entries form1+2+4 and so on, less than2n for positive n. Adding n element writes gives a linear aggregate bound. The example records resize events so the expensive individual steps remain visible. Python’s implementation can choose different growth details while supporting an appropriate amortized append contract.
def doubling_model(n):
if type(n) is not int or n < 0:
raise ValueError("Count must be a nonnegative integer")
size = capacity = cost = 0
resizes = []
for _ in range(n):
if size == capacity:
new_capacity = max(1, 2 * capacity)
cost += size
resizes.append((size, new_capacity))
capacity = new_capacity
size += 1
cost += 1
return cost, resizes
print(doubling_model(8))
print(doubling_model(0))
(15, [(0, 1), (1, 2), (2, 4), (4, 8)])
(0, [])
Eight writes plus seven copied elements cost15 in this model. No runtime allocation size or benchmark timing is measured.
Charge each queue element for one transfer
The second program implements FIFO using an incoming stack and an outgoing stack. New items enter incoming. When outgoing is empty, transferring all incoming items reverses their order so the oldest reaches the outgoing top. Each element crosses from incoming to outgoing at most once before removal. Across n pushes and at most n successful pops, transfers are at most n, even though one pop can transfer many elements. This aggregate proof assumes the underlying stack operations have their stated costs; with dynamic-array stacks, their own amortized bounds must also be included consistently.
class TwoStackQueue:
def __init__(self):
self.incoming, self.outgoing = [], []
self.moves = 0
def push(self, value):
self.incoming.append(value)
def pop(self):
if not self.outgoing:
while self.incoming:
self.outgoing.append(self.incoming.pop())
self.moves += 1
if not self.outgoing:
raise IndexError("Queue is empty")
return self.outgoing.pop()
queue = TwoStackQueue()
queue.push("A")
queue.push("B")
first = queue.pop()
queue.push("C")
second, third = queue.pop(), queue.pop()
print(first, second, third)
print(queue.moves)
A B C
3
C waits while B is already in outgoing. Each of A,B,C transfers once and preserves FIFO order.
Potential and accounting explain the same conserved work
An accounting proof can charge extra credit to each push and spend that credit when its item transfers. A potential function can instead measure stored future work, such as a multiple of incoming-stack size. The amortized cost is actual cost plus potential change. The potential should remain bounded below under the chosen starting state so total actual work is controlled. Do not invent negative credit that makes every operation appear free without paying initial or terminal obligations. The goal is a valid sequence inequality, not a cosmetic assignment of smaller numbers to expensive operations.
Select algorithms from constraints and required outputs
A comparison-based full sort has different lower-bound assumptions from integer counting or radix methods. Producing every matching pair can require quadratic output even when finding one pair is linear under hashing assumptions. A streaming top-k query differs from sorting all input, and static range queries differ from online updates. Write the output and operation mix before comparing asymptotic labels. Include memory, preprocessing, key cost, numeric representation, worst-case requirements, and failure policy. A theoretically attractive bound does not excuse an invalid precondition or a result contract weaker than the user’s task.
Use deterministic counters and independent references
For the doubling model, check total cost grows within the proved linear envelope over small n and inspect resize boundaries separately. For the queue, compare finite mixed push/pop sequences with collections.deque and verify no item transfers twice. Record operation counts separately from wall-clock time; tiny timing results may mostly reflect interpreter overhead. The course’s isolated Python execution and finite differential checks establish exact outputs and tested properties, not a production latency benchmark or a proof over every possible input. Keep those limits visible when reporting algorithm improvements.
Common errors and fixes
Amortized constant cost is promised as constant latency
- Why it happens
- A total-sequence bound was applied to every individual operation.
- How to fix it
- State both the occasional worst case and the aggregate bound.
- Prevent it next time
- Inspect resize and transfer-triggering operations.
A doubling model is reported as CPython measurement
- Why it happens
- Abstract counted work was confused with implementation observation.
- How to fix it
- Label the modeled policy and measure implementations separately if needed.
- Prevent it next time
- Attach model assumptions to every cost result.
Only input size is counted for a huge output
- Why it happens
- The result contract’s output lower bound was omitted.
- How to fix it
- Include output-sensitive work and space in analysis.
- Prevent it next time
- Contrast find-one with enumerate-all tasks.
Check your understanding
1. Why can one queue pop be linear while a whole sequence remains linear?
Show worked solution
That pop may transfer all waiting incoming items. Each transferred item then stays in outgoing until removed and is never transferred again. Summing across the sequence charges each item once, so occasional bursts do not multiply into quadratic total work.
2. Does expected hashing performance mean the same as amortized resizing?
Show worked solution
No. Expected analysis uses a probability model, while amortized analysis bounds a sequence even without random input under its operation assumptions. One implementation may involve both. State each source of the guarantee rather than using average as an undefined substitute.
Key takeaways
- One expensive operation can fit a cheap total sequence
- Use deterministic counters and independent references