liveBook · Manning

Amortized analysis

As explained in Fabulous Adventures in Data Structures and Algorithms

Amortized cost is the average resource cost per operation over a sequence of operations, including occasional expensive operations.

A slow operation does not necessarily make a data structure slow to use. Amortized analysis measures the total cost of a sequence of operations and divides it across those operations. The question is not whether every call is cheap, but whether expensive calls happen often enough to dominate the total. section 2.4

A queue made from two stacks

An immutable queue can enqueue onto one stack and dequeue from another. When the dequeue stack runs out, reversing an enqueue stack of n items costs O(n) time and space. Yet an item is pushed and popped only twice on its way through the queue. Those four constant-time actions give O(1) amortized cost per enqueue or dequeue. Equivalently, a reversal is followed by n cheap dequeues: O(n) work spread across roughly n calls. section 2.4

The qualification matters: amortized O(1) is not worst-case O(1). Persistence also changes which sequences are possible. Enqueue 100 items, perform a costly dequeue, undo to the earlier queue, and repeat that same dequeue. Each attempt reverses the same stack again; no run of cheap dequeues pays for it. If that pattern matters, options include choosing a structure with a better worst case or memoizing the reversal at a memory cost. section 2.6.3

Make the expensive case rarer—and smaller

In an immutable deque, pushes and pops usually update shallow levels. Two-thirds update one level, two-ninths update two, and progressively fewer reach deeper levels. Averaging those costs gives O(1) amortized time, while even a worst-case push or pop takes only O(log₃ n) time and extra space. Allowing two-item groups in the middle changes the depth bound to O(log₂ n), but leaves amortized pushes and pops O(1). section 3.5 section 3.8

The same distinction appears when generating lexicographic permutations of n distinct items. A call can require O(n) comparisons and swaps, but across all n! permutations, cheap calls predominate: the averages are three comparisons and 1.5 swaps per permutation, yielding O(1) amortized time. Always state the sequence over which the average is taken. section 6.2.1

Listing 6.17 An elegant recursive method for generating combinations
static IEnumerable<IDeque<int>> Combinations2(int n, int k)
{
    if (k > n || k < 0)
        yield break; #1
    if (k == 0)
    {
        yield return Deque<int>.Empty; #2
        yield break;
    }
    foreach (var r in Combinations2(n - 1, k)) #3
        yield return r; 
    foreach (var r in Combinations2(n - 1, k - 1)) #4
        yield return r.PushRight(n-1);
}

The recursive combination generator shows why a cheap component operation is not the whole algorithm: each result uses k deque pushes at O(1) amortized cost apiece, yet recursion makes the overall worst-case bound O(n) per combination. section 6.3.1

How Fabulous Adventures in Data Structures and Algorithms covers it

How deep it goes
Works through per-item accounting and operation frequencies, then tests where persistence defeats the queue argument.
Examples
Uses stack reversal during dequeue, repeated dequeue after undo, and the distribution of deque updates.
What it assumes
Assumes familiarity with stacks and big-O notation while defining amortized cost through a concrete queue.
What you can do after
Evaluate/understand when a sequence supports an amortized bound and when repeated access to a persistent state breaks the argument.
How it connects
Connects immutable queues, persistent data structures, undo and redo, immutable deques, and lexicographic permutations.

Also explained in

Compare how 3 books explain amortized analysis →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Fabulous Adventures in Data Structures and Algorithms. The section links point to the parts of the book it draws on.