python/volume-3/chapter-46ch 46 / 64 · 12 min

46Randomization & the probabilistic mind

In Chapter 45 we turned strings and number theory into the machinery of crypto and quant. Every algorithm we've built this volume has made its choices deterministically: the same pivot, the same hash, the same next node, every single time. Now we do the one thing that sounds like surrender but turns out to be a superpower — we flip a coin. Here's the plan. We'll armor a sort so no attacker can find its worst case, compute a number like π by throwing darts at a wall, price a financial option by simulating a million possible futures, and draw a perfectly fair sample from a stream too big to hold. One question drives the whole chapter: how can deliberately adding randomness make an algorithm more reliable, not less? By the end you'll reach for a coin on reflex — when being predictable is the weakness, when the exact answer is out of reach, when you need fairness over data you can't even count.

Every fixed plan has a nemesis Randomness is armor — the randomized pivot Compute by throwing darts — Monte Carlo The √N law — and pricing the future
scroll to begin

01Every fixed plan has a nemesis

Let's start with a word we've leaned on all volume without saying it out loud: deterministic. A deterministic algorithm always makes the same choice on the same input. Feed it the same list twice and it does the exact same thing, step for step. Usually that's a virtue — it's what makes code testable and predictable. But watch what it costs us. A deterministic algorithm has a fixed worst case: one specific arrangement of input that drives it to its slowest behavior, every time. And if someone can read your code — it's open-source, or a public API, or a well-known library — they can construct that exact arrangement and hand it to you on purpose.

Quicksort is the classic victim. Tell it "always pick the first element as the pivot," then hand it an already-sorted list. Watch it unravel. Every partition peels off just one element, the recursion sinks n levels deep, and the tidy O(n log n) collapses into O(n²). (You stepped quicksort's partition in trace T28 — here we attack it.) That's not a thought experiment. It's a denial-of-service. Put a naive quicksort behind a web endpoint, and an attacker who sends sorted data can pin your CPU flat with a tiny request.

The escape is not a cleverer fixed rule. The adversary just studies the new rule and defeats that one too. The escape is to stop being predictable. If the algorithm flips a coin to make its choices, no single input is reliably its worst case — because the worst case now depends on coins the attacker cannot see.

Deterministic — predictable pivot = first element adversary reads the rule… feeds a sorted list O(n²) — every time Randomized — unaimable pivot = 🎲 random adversary reads the rule… and has nothing to aim at O(n log n) — expected
Fig — Determinism is a fixed target. The attacker who knows your rule can always find your worst input — unless your choices depend on coins they can't predict.
Algorithmic-complexity attacks are real
Sorted-input quicksort, hash flooding (the hashing chapter), and catastrophic regex backtracking (the backtracking chapter) are all the same exploit: force a deterministic algorithm into its worst case on purpose. Randomization is a standard defense against every one of them.

So how much does a single coin flip actually buy? Let's stop asserting and start counting →

02Randomness is armor — the randomized pivot

Count the fixed-pivot disaster first. Partitioning a block of m elements costs m−1 comparisons. If every pivot is the extreme value — exactly what a sorted list guarantees — the block sizes march down n−1, n−2, …, 1, so the total is (n−1)+(n−2)+…+1 = n(n−1)/2. That's the textbook O(n²), and it isn't approximate. I ran a comparison-counting quicksort on a sorted list of n = 2000, and the fixed-pivot version reported 1,999,000 comparisons — exactly n(n−1)/2, to the comparison.

Now make the pivot random. Each pivot is equally likely to be any rank, so partitions come out balanced on average, and the expected number of comparisons works out to about 2n ln n (≈ 1.39·n·log₂n) — squarely the O(n log n) class. Same sorted input, random pivot: about 24,600 comparisons, averaged over twenty runs. That's roughly 80× fewer — on the very input that murdered the deterministic version.

The deep move: we didn't change the input, we changed who chooses. The chance that a random shuffle lands on any one specific bad arrangement is 1/n!. For just 20 items that's 1 in 2.4×10¹⁸; for a shuffled deck of 52 cards it's 1 in 8×10⁶⁷ — more arrangements than there are atoms in the galaxy. The worst case didn't vanish. It just became something you will not witness before the heat death of the universe.

armor.pypython
def quicksort_count(a, pivot="first"):
    a = list(a); comps = 0; stack = [(0, len(a)-1)]
    while stack:
        lo, hi = stack.pop()
        if lo >= hi: continue
        if pivot == "random":                 # ← the one line that armors it
            p = random.randint(lo, hi); a[p], a[hi] = a[hi], a[p]
        pv = a[hi]; i = lo
        for j in range(lo, hi):
            comps += 1                         # count every key comparison
            if a[j] < pv:
                a[i], a[j] = a[j], a[i]; i += 1
        a[i], a[hi] = a[hi], a[i]
        stack.append((lo, i-1)); stack.append((i+1, hi))
    return comps
# sorted input, n=2000:  fixed → 1,999,000   random → ~24,600  (≈80× fewer)

Line by line: we sort a copy using an explicit stack instead of recursion, so the sorted worst case can't overflow Python's call stack. The if pivot == "random" line swaps a randomly chosen element into the pivot slot — that's the entire defense. The inner for loop is the partition, and comps += 1 tallies each key comparison, so we measure cost directly instead of trusting a formula. Everything else is ordinary Lomuto partitioning (stepped in T28).

fixed pivot, sorted input depth = n n(n−1)/2 compares random pivot depth ≈ log₂ n ≈ 2n ln n compares Same elements. The pivot choice alone decides whether the tree is a stick or a bush.
Fig — The cost of quicksort is the shape of its recursion tree. A fixed pivot on sorted data grows a one-sided stick (n levels); a random pivot grows a balanced bush (log n levels).
InteractiveSlide n — watch the worst case pull away from the expected one
fixed pivot 0 random pivot 0 bar length is log-scaled; the numbers are the real comparison counts
800
The gap between the two bars is the adversary's power — and randomizing the pivot gives it away.
The deeper cut — where 2n ln n comes from

Number the sorted values 1..n. Two values of rank i and j (with i<j) are compared only if one of them is chosen as a pivot before any value ranked strictly between them. An in-between pivot would split i and j into different partitions, and they'd never meet again. Among the j−i+1 values in that rank window, each is equally likely to be picked first, so the probability that it's i or j is 2/(j−i+1). Sum that expected indicator over all pairs and you get Σ 2/(j−i+1) ≈ 2(n+1)H_n − 4n ≈ 2n ln n, where H_n is the harmonic number. My measured 24,593 for n=2000 sits right on that curve — the leading 2n ln n ≈ 30,400 minus the O(n) correction. No worst-case input exists, because the cost is an average over the coins, not over the data.

↺ The thing people get backwards
Most people assume a random pivot makes quicksort faster on average than a good deterministic pivot. It doesn't — on random data, a plain median-of-three does just as well. What randomization buys is not speed, it's robustness: it strips the adversary of the power to choose your input. You trade a deterministic guarantee for a probabilistic one that no input can break. Randomness here is armor, not a turbocharger.
Where you meet this
Python's own sort is Timsort (the sorting chapter), not quicksort — but C-library qsort implementations, many database sort routines, and countless others randomize the pivot for exactly this reason. Python also randomizes string hashing per process (PYTHONHASHSEED) to defeat hash-flooding DoS, and "power of two random choices" load balancers spread traffic evenly by picking two servers at random and taking the emptier — randomness as armor, everywhere.

Armor is one use of a coin. The other is stranger: using randomness to compute a number you can't otherwise reach

03Compute by throwing darts — Monte Carlo

Now turn the idea inside out. Instead of using randomness to protect an exact algorithm, use it to estimate an answer that's hard to compute directly. The technique is called Monte Carlo, after the casino, and its motto is simple: to learn about a system, sample random outcomes from it and count.

The cleanest demonstration computes π. Draw a 1×1 square, and inside it the quarter of a circle of radius 1 centered at one corner. The square's area is 1; the quarter-circle's area is (1/4)·π·r² = π/4. Throw darts uniformly at random into the square. On average, the fraction that land inside the quarter circle must equal the ratio of the areas — π/4. So π ≈ 4 × (darts inside) / (total darts). A dart at (x, y) is inside exactly when x² + y² ≤ 1: one multiply, one compare, no trigonometry, and the symbol π appears nowhere in the code. You measure it by counting.

montecarlo.pypython
def estimate_pi(n):
    inside = 0
    for _ in range(n):
        x, y = random.random(), random.random()   # a dart in the unit square
        if x*x + y*y <= 1.0:                        # inside the quarter circle?
            inside += 1
    return 4 * inside / n                           # fraction inside ≈ π/4

#      n         estimate      error
#     100        3.120000     0.021593
#    1000        3.128000     0.013593
#   10000        3.126000     0.015593   ← note: WORSE than n=1000!
#  100000        3.137280     0.004313
# 1000000        3.140592     0.001001
# 10000000       3.142310     0.000717

Four lines do it. random.random() draws a coordinate in [0,1), so (x, y) is a uniform dart. The one comparison x*x + y*y <= 1.0 asks "is it inside?", inside tallies the hits, and 4 * inside / n turns the ratio into π. The table is a real run, and look closely: at n = 10,000 the estimate was actually worse than at n = 1,000. That's not a bug. A single random run can get lucky or unlucky. What improves reliably is never one run — it's the typical error, and pinning that down is the next section.

inside outside π ≈ 4 · (green / total) area ratio = π/4
Fig — No formula for π anywhere — just darts and a count. The geometry does the math: the fraction landing in the quarter circle is π/4.
InteractiveThrow more darts — watch π assemble itself out of noise
darts thrown 500 estimate of π 3.1416 error vs true π 0.0000
500
You just computed π with no formula — only counting. More darts, tighter answer (drag it back and forth: the noise is real).
Wait —
a million darts only bought me three correct digits. How many would I need for six?

04The √N law — and pricing the future

Here's the law that governs every Monte Carlo estimate, and it's worth burning into memory: the typical error shrinks like 1/√N, where N is the number of samples. I measured it directly. Run the π estimator 200 times at N = 100, and the estimates scatter with a standard deviation of about 0.165. Quadruple to N = 400 and the scatter halves to 0.081. Quadruple again to 1600 and it's 0.039; once more to 6400 and it's 0.020. Every ×4 in samples halves the error — the fingerprint of 1/√N.

Flip that around and it stings. To win one more correct decimal digit — 10× smaller error — you need 100× more samples. Monte Carlo hands you the first digits cheaply and charges brutally for the last ones. Why 1/√N? Each sample is an independent draw, and averaging N independent draws is exactly the setup where the spread of the average narrows as 1/√N. It's the same statistics that make a poll of 1,000 people accurate to a few percent, not a few thousandths. This isn't special to π. It's the signature of averaging independent randomness.

10²1 digit 10⁴2 digits 10⁶3 digits 10⁸4 digits 10¹⁰5 digits 10¹²6 digits bar height is log-scaled — each step right is a 100× taller sample budget
Fig — The cruel arithmetic of 1/√N: every extra correct digit multiplies the required samples by 100. Three digits is a million darts; six digits is a trillion.
InteractiveSlide the sample budget — feel the diminishing returns
samples N (log scale) → typical error → N = 10,000 typical error ≈ 0.016 one more digit → ×100 samples
10,000
The curve is 1.6/√N (the constant measured from the real π run) — steep at first, then agonizingly flat.

And this is exactly why Wall Street runs on Monte Carlo. The fair price of a financial option is an expected value — the average payoff across every possible future path of the underlying stock, discounted back to today. For the simplest options there's a closed-form answer (the Black–Scholes formula). For the messy real ones — path-dependent, many assets, exotic payoffs — the integral has no formula at all. So you don't solve it; you sample it: simulate thousands of random futures, compute the payoff in each, and average. I priced a European call both ways. Black–Scholes says 10.4506. Monte Carlo, averaging simulated futures, gives 11.02 at 1,000 paths, 10.49 at 100,000 paths, and 10.4458 at a million paths — the √N law crawling toward the true price. Risk desks simulate millions of paths every night to answer "how much could we lose tomorrow?" That's expected-value thinking made computational: when you cannot reason about the average, sample it.

ex3_out.pypython
def mc_call(n, S0, K, r, sigma, T):
    drift = (r - 0.5*sigma**2)*T
    vol   = sigma*(T**0.5)
    total = 0.0
    for _ in range(n):
        z  = random.gauss(0, 1)               # one random shock
        ST = S0*math.exp(drift + vol*z)        # one simulated future price
        total += max(ST - K, 0.0)              # the call's payoff on that path
    return math.exp(-r*T) * total/n            # discounted average payoff
# Black–Scholes = 10.4506;  mc_call(1_000_000) → 10.4458

The loop is the whole idea. random.gauss(0, 1) draws one random "shock", S0*exp(drift + vol·z) turns it into one possible price at expiry, and max(ST − K, 0) is what the call pays on that future. Average over a million futures, discount to today, and you've evaluated an integral no one wrote down — by sampling it.

✗ The myth

"More samples always means a better answer, so if it's not accurate enough, just crank N higher."

✓ The reality

Error falls only as 1/√N — the tenth digit would cost 10¹⁸ samples. Past a point you stop brute-forcing and switch to variance reduction (antithetic variates, control variates, low-discrepancy sequences), which shrinks the constant in front of the 1/√N instead.

Where you meet this
Every path-traced movie frame and every game's global illumination is Monte Carlo integration over light rays — the grainy "noise" in a half-rendered image is the 1/√N error, and it clears as more rays are cast. The method was born at Los Alamos in the 1940s for neutron diffusion; today it powers physics simulation, A/B testing, Bayesian inference (MCMC), and — everywhere — finance.
Why the machine loves it
Monte Carlo is embarrassingly parallel: every sample is independent, so you split N draws across cores — or thousands of GPU lanes — with zero locking or coordination (recall the GIL tradeoffs from Volume 2). That's why render farms and risk grids throw enormous core counts at it, and the 1/√N error falls in wall-clock time roughly in proportion to the hardware you own.

Quicksort's coins and π's darts gamble in opposite directions. Naming that difference is the whole taxonomy of randomized algorithms →

05Two ways to gamble — Las Vegas vs Monte Carlo

Look at what the coins actually put at risk. Randomized quicksort always returns a correctly sorted list — the coins only affect how long it takes. A bad streak makes it slower, never wrong. The π estimator is the mirror image. It always finishes in exactly N steps, but the answer is only approximate; a bad streak makes it wrong-ish, never slow. Those are the two families of randomized algorithm, and knowing which one you're holding tells you exactly what can go wrong.

Las VegasMonte Carlo
Answeralways correct ✓probably correct / approximate
Running timerandom (usually fast)fixed & bounded
You bet…timecorrectness
Examplesrandomized quicksort, quickselectπ by darts, option pricing, Miller–Rabin
Las Vegas answer: LOCKED ✓ time: 🎲 varies keep rolling until right — correctness is never at risk Monte Carlo answer: 🎲 varies time: LOCKED stop on a clock — accept a tiny chance of error
Fig — Each family locks one thing and lets the coin move the other. Las Vegas bets time to guarantee the answer; Monte Carlo bets the answer to guarantee the time.

There's a beautiful bridge between them. If you can check an answer cheaply, you can convert Monte Carlo into Las Vegas. Guess with a fast randomized method, then verify the guess; if it's wrong, guess again. The verify step turns "probably right" into "definitely right, eventually" — you've traded a small error probability for a small bit of extra runtime.

The deeper cut — tunable doubt (Miller–Rabin)

The Miller–Rabin primality test from the number-theory chapter is Monte Carlo with a dial. For a composite number, a single random "witness" exposes it as composite with probability at least 3/4 — so after one test, a prime-looking number could still be a fraud with probability at most 1/4. Run k independent tests and the chance of being fooled every time drops to at most (1/4)ᵏ = 4⁻ᵏ. At k = 30 that's under 10⁻¹⁸ — you're likelier to be struck by a cosmic-ray bit-flip than to certify a composite as prime. You don't wait for certainty; you buy as much of it as you want, one cheap random test at a time.

InteractiveBuy certainty by the flip — watch your doubt collapse ×¼ a test
each independent test multiplies your remaining doubt by ¼ k = 15 tests → a fake passes ≤ 1 in 1.07 × 10⁹ times rarer than winning a national lottery jackpot 10³ 10⁶ 10⁹ 10¹² 10¹⁵ 10¹⁸ · sand 10²¹ 10²⁴ meter length = orders of magnitude of certainty — each test buys ~0.6 more Certainty isn't found — it's bought, one cheap coin-flip at a time.
15
A single Miller–Rabin test lets a composite slip through with probability at most ¼; k independent tests fail together only with probability 4⁻ᵏexponential, not the crawling 1/√N two sections up. Slide to 30 and you're likelier to hit a cosmic-ray bit-flip in RAM than to certify a fraud.
Wait —
can I take a perfectly fair random sample from a stream when I don't even know how long it is — and can't store it?

06A fair sample from an endless stream — reservoir sampling

The problem sounds impossible. You're watching a firehose — every tweet, every log line, every sensor reading — and you want to keep a uniformly random sample of, say, one item. When the stream finally ends, every item that ever flowed past should have had an equal chance of being the one you kept. The catch: you don't know how long the stream is (it could be billions of items, or never end), you get exactly one pass, and you have O(1) memory — room for the one item, nothing more.

The solution is one line of genius. Reservoir sampling: keep the first item. When the i-th item arrives, replace whatever you're holding with it with probability exactly 1/i. That's the entire algorithm — and notice that n, the stream's length, never appears.

reservoir.pypython
def reservoir_one(stream):
    kept = None
    for i, item in enumerate(stream, start=1):   # i = 1, 2, 3, ...
        if random.random() < 1.0 / i:            # newest barges in w.p. 1/i
            kept = item
    return kept
# length-10 stream, 1,000,000 trials — how often each item was the survivor:
#   item 0: 10.00%   item 1: 10.04%   item 2:  9.98%   item 3:  9.99%
#   item 4:  9.97%   item 5: 10.00%   item 6: 10.06%   item 7:  9.98%  ...
# every item ≈ 1/10, dead fair — with room for exactly one item in memory

The loop reads one item at a time. At position i, random.random() < 1.0/i is true with probability 1/i. So item 1 is kept for sure, item 2 has a 1-in-2 shot at bumping it, item 3 a 1-in-3 shot, and so on. The comment is a real million-trial run: every one of the ten items ended up the survivor almost exactly a tenth of the time, deviating by at most 0.06 percentage points. Perfectly fair, in one pass, in constant memory.

the stream flows past → #11/1 #21/2 #31/3 #41/4 #i1/i reservoir holds ONE item (1/i)·(i/(i+1))·((i+1)/(i+2))···((n−1)/n) = 1/n the product telescopes — every numerator cancels the previous denominator
Fig — Why it's fair: an item kept at step i must then survive every later replacement. Multiply the survival chances and the whole product collapses to exactly 1/n — the same for the first item and the last.

Here's the magic, worked out. Consider item i. It gets kept at step i with probability 1/i. To still be the survivor at the end, it must then not be replaced by items i+1, i+2, …, n. Item j replaces with probability 1/j, so it fails to replace with probability (j−1)/j. Multiply the whole chain of survival:

(1/i) · (i/(i+1)) · ((i+1)/(i+2)) · … · ((n−1)/n) = 1/n

and it telescopes: every numerator cancels the denominator before it, leaving exactly 1/n. Every item, whether it arrived first or billionth, ends with probability 1/n. The math is fair by construction, and n is never needed.

InteractiveWalk down the stream — watch the newcomer's odds shrink
item #i arrives and barges into the reservoir with probability 1/i: 100% i = 1 → 1/1 = 100.0% …and after the algebra, every one of the i items seen so far is holding an equal 1/i share. each item so far: 1/1 = 100.0%
1
The newcomer's odds fall as 1/i — and that's exactly what keeps the whole sample fair.
Where you meet this
"A random sample of all tweets," Spark's and BigQuery's TABLESAMPLE, random log sampling for observability dashboards, and picking a random line from a file too big for RAM — all reservoir sampling. Any time a system shows you "a random selection" from something it can't hold all at once, this one-pass 1/i trick is very likely running underneath.
The deeper cut — keeping k items (Algorithm R)

For a sample of k items, fill the reservoir with the first k. Then for the i-th item (0-indexed, i ≥ k), pick a random slot j in 0..i; if j < k, overwrite res[j] with the new item. The same telescoping argument gives every item a final probability of exactly k/n. I ran it — k = 5 from a stream of 100, over 200,000 trials — and every item appeared as a chosen one about 10,000 times out of 200,000, i.e. k/n = 5% each, flat across all 100. One pass, O(k) memory, provably uniform.

Step back and see the shape of this whole chapter. Randomness bought three different things: armor (no adversary can aim at a random pivot), computation (sample the darts, the futures, the light rays, and average your way to an answer no formula gives), and fairness under ignorance (a uniform sample without ever knowing how much there is). The transferable move — the one the 1% run on reflex — is this: when being predictable is your weakness, add a coin; when the exact answer is out of reach, estimate it by trying many random cases and averaging; and when you want a property like fairness, work backwards from the property to the rule that produces it. Stanislaw Ulam invented Monte Carlo not by out-computing the neutron equations but by asking a humbler question. Recovering from illness and playing solitaire, he wondered about his odds of winning, and realized that instead of grinding through the combinatorics he could just deal many random hands and count. That question — "what if I just try it thousands of times and count?" — is available to you on every hard problem you'll ever meet.

Six chapters of patterns; one volume of thinking. Next we assemble them into a single operating system for the algorithmic mind — how to look at any new problem and know, in seconds, which technique it wants. Chapter 47 is the difference between the 99% and the 1%, and it closes the volume →

PROGRAMS run them line by line — the same ideas, written the many ways Python offers
Chapter 46, in working code

Twelve tiny programs where a single coin flip changes everything — armor against the adversary who studies your code, a way to measure π by throwing darts, and fair samples pulled from a stream you only get to see once.

Randomness is armor — the randomized pivot
A deterministic algorithm has one fixed worst case an adversary can hand you on purpose. Flip a coin to choose, and there is nothing left to aim at — same input, no disaster.
Compute by throwing darts — Monte Carlo
Turn randomness inside out: sample random outcomes and count to MEASURE a number you can't easily compute. Cheap first digits, and a 1/√N law that charges brutally for the last ones.
Two ways to gamble — Las Vegas vs Monte Carlo
Las Vegas is always correct and gambles its running time; Monte Carlo is always fast and gambles its correctness. Same coin, opposite bargains.
Fair samples, fair shuffles, fair spreads
The everyday face of randomization: pick one item from an endless stream, prove it's uniform, shuffle a deck without bias, and flatten load across servers — all with a handful of coin flips.
end of chapter 46 · six sections
iolinked.com
Written by Ajai Raj