Module 04 — Random Numbers
Source: The Art of Computer Programming, Vol. 2, 3rd ed., Ch. 3 — §3.1 (what randomness means), §3.2.1 (the linear congruential method and Theorem 3.2.1.2A), §3.3.1 (the chi-square test), §3.4.2 (random sampling and shuffling). Lab:
labs/module-04-random· Grade it:./grade 4Concrete Mathematics companion: Chapter 8 (Discrete Probability) for expectation and randomized analysis, Chapter 4 for the number theory behind full-period generators — see ../../docs/concrete-mathematics.md.Self-contained: you can complete the module without the book. If you own Vol. 2, read §3.2.1 and §3.3.1 first — this lesson tells you where to look.
"Anyone who considers arithmetical methods of producing random digits is, of course, in a state of sin." Knuth quotes von Neumann on the first page of Chapter 3, and the joke is also the whole problem: a deterministic program cannot produce a random sequence, only a sequence that behaves random enough for the job. This module builds the classic engine (a linear congruential generator), a scientific instrument to judge it (the chi-square test), and two algorithms that consume randomness correctly (shuffling and reservoir sampling) — plus the counting arguments that prove the good ones fair and the plausible-looking ones biased.
Companion exhibit — The Illusion of Chaos. The linear congruential generator you build here has an interactive twin in the Museum of Algorithms: the Loom of Chaos plots consecutive pairs and lets you drag a "spectral dial" on the multiplier — a few coarse stripes for a bad
a, dissolving into white noise for a good one. It runs exact BigInt arithmetic and shows the Hull–Dobell full-period verdict live. Reach it from the Museum tab in the top navigation.
1. Pseudo-randomness: the caveat (§3.1)
A random sequence is an idealization; what a program emits is a pseudo-random sequence — fully determined by its starting conditions, yet passing the statistical tests we would apply to a truly random one. Two traps frame the subject:
- Complex random. Von Neumann's own "middle-square" method (square the number, take the middle digits) looks chaotic and is terrible: it quickly falls into short cycles or the fixed point 0. Complexity of the recipe is no guarantee of quality of the output.
- Randomness is a property of the process, not any one sequence. No finite string is "random" or "not random" by itself. We can only ask whether a generator produces output indistinguishable, under stated tests, from independent uniform draws.
Because the state is finite (say all 64-bit words), the sequence must eventually repeat: it is ultimately periodic. A short tail may lead into a cycle, and the cycle length is the period. A usable generator needs, at minimum, a period far longer than the amount of output you will draw — but a long period is necessary, never sufficient.
2. The linear congruential method (§3.2.1)
Lehmer's 1949 generator is still the textbook workhorse. Pick four magic quantities: the modulus , the multiplier , the increment , and the seed . Then
Algorithm L (one LCG step)
L1. [Multiply and add.] Set X <- a·X + c.
L2. [Reduce.] Set X <- X mod m, and output X.
Two implementation notes carried by the lab's Lcg:
- The word-size modulus. Taking makes the "mod" free: it is
exactly the wrap of 64-bit machine arithmetic. The lab encodes this as
m == 0and useswrapping_mul/wrapping_add. For any otherm > 0the producta·X + cis formed in 128-bit arithmetic before reducing, so it cannot overflow. - Indexing.
newstores ; the firstnext()returns . Knuth traces sequences from , and so do the tests.
Trace (§3.1's opener). , :
| n | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| 7 | 6 | 9 | 0 | 7 | 6 |
The sequence is 7, 6, 9, 0, 7, 6, 9, 0, … — period 4. Only ten values were possible, and we squandered more than half of them. The lesson of the example: parameters must be chosen, not guessed.
2.1 Full period — Theorem 3.2.1.2A
We want the longest possible period, , achieved from every seed (so the generator visits all of before repeating). This happens exactly under three conditions.
Theorem 3.2.1.2A. The linear congruential sequence has period length if and only if
- is relatively prime to ();
- for every prime dividing ;
- if 4 divides .
Proof sketch. The three conditions are exactly what is needed for the map to be a single -cycle. Condition 1 rules out an increment that shares a factor with (which would trap the sequence in a coset). Conditions 2–3 control the multiplier: writing the closed form , one analyzes, prime power by prime power via the Chinese Remainder Theorem, when and the geometric-sum factor first returns to 0. For a prime power the order works out to require , with the extra needed at the prime 2 because the group of units mod is not cyclic for . Knuth gives the full number-theoretic argument in §3.2.1.2; the payoff is a clean, checkable recipe. ∎
Worked predicate, . The only prime dividing is 2, and ,
so conditions 2–3 collapse to , i.e. ; condition
1 says must be odd. So full period () and ( odd). For example
period(5, 3, 16, 0) = 16, while period(3, 1, 16, 0) < 16 () and
period(5, 2, 16, 0) < 16 ( even). Stage 1 verifies the entire "if and only
if" by enumerating all 256 pairs.
Worked predicate, . Primes 2 and 5 divide , and , so and , i.e. (); and . Try , : full period 100 from any seed.
2.2 The low-order-bit weakness
When , the low bits of an LCG are far less random than the high bits. Reducing modulo keeps only the bottom bits, and
depends only on . So the last bits are themselves a little LCG
with modulus — and hence have period at most . The lowest bit of a
maximal-period power-of-two LCG simply alternates 0,1,0,1,…; the low two bits
cycle with period 4; and so on. Never derive a small integer with
X mod k (or, worse, use the low bit as a coin flip). Instead take the high
bits. The lab's test closures use the multiply-shift map (Lemire's method),
rng(bound) = (X · bound) >> 64 , (X a 64-bit output)
which is : it reads the top bits of X and lands
uniformly in 0..bound.
2.3 The spectral flaw: RANDU and the 9−6−1 identity
IBM's notorious RANDU used , , . Its period issues aside, it has a fatal structural defect. Compute modulo :
a² = (2¹⁶ + 3)² = 2³² + 6·2¹⁶ + 9
= 2·2³¹ + 6·(2¹⁶ + 3) − 18 + 9
= 2·2³¹ + 6a − 9 ≡ 6a − 9 (mod 2³¹).
Since (with ) and modulo ,
X_{n+2} ≡ (6a − 9) X_n = 6 X_{n+1} − 9 X_n (mod 2³¹),
i.e. 9 X_n − 6 X_{n+1} + X_{n+2} ≡ 0 (mod 2³¹) for all n.
Every consecutive triple , plotted as a point in the unit cube, therefore satisfies one linear equation — so all the points lie on just 15 parallel planes. This is the crude end of §3.3.4's spectral test, which measures exactly how far apart such planes are for a given multiplier. Stage 1 checks the identity holds for 200 consecutive outputs. Good multipliers make the lattice of points as fine and isotropic as possible; RANDU is the cautionary tale of what a bad one does.
3. Judging a generator: the chi-square test (§3.3.1)
We need to measure "random enough." The chi-square test compares observed category counts against a probability model. Suppose independent observations each fall in one of categories, category having model probability (so ). Let be the observed count in category ; the model expects . Define the statistic
Each term measures a squared deviation, scaled by the expected count so that naturally large categories are not unfairly penalized. near 0 means an almost suspiciously perfect fit; very large means the model is a poor description of the data.
How large is "too large"? Under the model, is approximately distributed as a chi-square variate with degrees of freedom (one is lost to the constraint ). We reject the model when exceeds a high percentile of that distribution. A tiny table of the row (used by stages 2–4):
| percentile | 1% | 25% | 50% | 75% | 95% | 99% |
|---|---|---|---|---|---|---|
| chi-square, | 2.09 | 5.90 | 8.34 | 11.39 | 16.92 | 21.67 |
So with 10 categories a fair generator should give below about 99% of
the time. Rule of thumb (Knuth): every expected count should be ,
or the chi-square approximation is unreliable — merge sparse categories first.
The lab's chi_square panics on a non-positive expected count for this reason.
Worked example (two dice). Throw two dice 144 times; category has probability from the familiar 1,2,3,4,5,6,5,4,3,2,1 over 36, so the expected counts are 4, 8, 12, 16, 20, 24, 20, 16, 12, 8, 4. For the observed table 2, 4, 10, 12, 22, 29, 21, 15, 14, 9, 6,
With , that sits between the 25% and 50% points — an unremarkable, and
therefore believable, fit. chi_square_uniform is the special case where every
, so each category expects ; e.g. counts give
.
4. Consuming randomness
4.1 Shuffling — Algorithm 3.4.2P (Fisher–Yates)
Given rng(bound) uniform in 0..bound, permute items uniformly in place.
In 0-based terms:
P1. [Initialize.] Set j <- t − 1.
P2. [Generate U.] Set k <- rng(j + 1) (uniform in 0..=j).
P3. [Exchange.] Swap items[k] <-> items[j]; then j <- j − 1;
if j >= 1, return to P2.
The candidate range shrinks: position swaps only with a position in . That is the entire trick.
Theorem. Algorithm P produces each of the permutations with probability exactly .
Proof (induction on ). For there is one permutation, probability 1. Assume the claim for . In the first pass P chooses uniformly from the positions and moves that element to the last slot; each of the choices has probability , and each fixes a distinct final element. The remaining elements occupy positions and are then shuffled by the same algorithm on a slice of length , which by hypothesis produces each of their arrangements with probability . A given permutation of all items determines its last element (probability ) and then an arrangement of the rest (probability ), so its total probability is . Since the outcomes partition all permutations, each occurs with probability . ∎
Equivalently: P consumes one draw from each of the bounds , giving equally likely "tapes", and the argument shows the map tape permutation is a bijection onto the permutations. Stage 3 enumerates the 6 tapes for (all distinct outputs) and, for , drives 43 200 shuffles ( per permutation) through the stage-2 chi-square test over all 720 permutations (, comfortably below the that marks the 99% point).
4.2 Why the "naive" shuffle is biased (a counting proof)
A tempting variant draws from the full range at every step:
for i in 0..n: swap items[i] <-> items[rng(n)] .
It is not uniform, and the proof is pure counting. There are independent draws each with outcomes, hence equally likely tapes. If every one of the permutations were equally likely, would have to divide . But for it does not: contains the prime factors of , which (a power of ) generally lacks. For : tapes, permutations, and is not an integer — so the six permutations cannot be equally likely.
Enumerating all 27 tapes gives the exact distribution (lexicographic order of the resulting permutation of [0,1,2]):
| permutation | 012 | 021 | 102 | 120 | 201 | 210 |
|---|---|---|---|---|---|---|
| tapes | 4 | 5 | 5 | 5 | 4 | 4 |
Never the fair . Stage 3 reproduces this table exactly. The moral: an algorithm that "obviously" mixes everything can still be provably biased — you must count.
4.3 Reservoir sampling — Algorithm 3.4.2R
Choose items uniformly from a stream whose length is unknown in advance, in one pass and memory. With = number of records seen so far:
R1. [Initialize.] Fill the reservoir with the first k records; set t <- k.
R2. [Next record.] If the stream is exhausted, stop: the reservoir is the
sample. Otherwise read a record and set t <- t + 1.
R3. [Draw.] Set M <- rng(t) (uniform in 0..t).
R4. [Replace?] If M < k, put the new record into reservoir[M]. Go to R2.
Theorem (the invariant). After records have been read, every one of them is in the reservoir with probability exactly .
Proof (induction on ). Base : the first records fill the reservoir, each present with probability . Inductive step: assume the claim after records; consider record . In step R3 we draw uniform in .
- The new record is kept iff , probability . ✓
- An old record was present with probability (hypothesis). It survives unless the new record displaces its exact slot: the new record enters with probability , and given that, lands on any of the slots with probability , so it evicts a particular old element with probability . Thus the old element remains with probability , giving total . ✓
So after records every record has probability ; induction completes the proof. When the stream finally ends at length , each item is in the sample with probability , and every -subset is equally likely. ∎
Stage 4 checks exact uniformity for , (the 12 tapes split the 6 subsets 2 apiece) and, over 30 000 LCG-driven trials with , , confirms each item appears with frequency via the chi-square test.
5. Stage-by-stage lab guide
Open labs/module-04-random/src/lab.rs. Run ./grade 4; stages are taken in
order, stopping at the first failure.
Stage 1 — Lcg and period
Implement new (reduce seed/a/c mod m when m > 0; store as-is when m == 0),
next (wrapping arithmetic for m == 0, u128 otherwise), and period (direct
cycle detection: remember the step each state was first seen; the first repeat
closes the cycle, so the tail is excluded automatically). Panic in period
when m == 0 — the message must contain "finite modulus". Tests reproduce §3.1's
opener, the MMIX constants, the full "iff" of Theorem 3.2.1.2A for and
, and RANDU's 9−6−1 identity.
Stage 2 — chi_square, chi_square_uniform
. Panic on length mismatch ("same number"), empty input
("at least one category"), non-positive expected ("positive"), and all-zero
counts ("at least one observation"). chi_square_uniform builds equal expected
counts and delegates. Anchored on the two-dice .
Stage 3 — shuffle, naive_shuffle
Algorithm P with running backwards from to 1; leave slices of length
untouched (don't touch the rng). Implement naive_shuffle faithfully wrong
(full-range swap) — the test proves its bias on all 27 tapes.
Stage 4 — reservoir_sample
Algorithm R. If k == 0 return empty without drawing; if the stream is shorter than return everything in order. Otherwise fill, then for each further record draw in and overwrite slot when .
6. Check your understanding
- The generator , has period 4. Which condition of Theorem 3.2.1.2A fails? ( is coprime to 10 ✓; ? no — fails condition 2 at , and also .)
- Why is
X % 2a terrible coin flip for a power-of-two-modulus LCG, but(X · 2) >> 64fine? (The low bit has period ; the high bits carry the full period.) - For the two-dice example with — do you reject the generator? (No: it is near the median, a perfectly ordinary fit.)
- In Algorithm P, what breaks if step P2 draws from
rng(t)(the full range) every time instead ofrng(j+1)? (You get the biased naive shuffle.) - In reservoir sampling, why does the new -th record enter with probability rather than ? (It enters whenever , and is uniform over values.)
7. Exercises from the text
Ratings use Knuth's scale: 00 immediate · 10 a minute · 20 up to an hour · 30
hours · 40 term project · 50 open problem. ▶ marks especially instructive ones.
Log attempts in course/module-04-random/exercises.md.
| Ex. | Rating | Statement (paraphrased) |
|---|---|---|
| 3.2.1.2-1 | 10 | Show the period of an LCG is at most , and find a case where it is exactly . |
| ▶3.2.1.2-4 | M23 | Prove the "if" direction of Theorem 3.2.1.2A for a power of 2. |
| 3.2.1-2 | 10 | For what seeds does , enter its cycle, and how long is any tail? |
| 3.3.1-8 | 20 | Compute for a given table and decide whether to accept the hypothesis. |
| ▶3.4.2-8 | 22 | Prove Algorithm P generates each permutation with probability (you did — stage 3; write it up). |
| 3.4.2-10 | M25 | Analyze the expected number of moves (slot replacements) reservoir sampling makes over a stream of length . |
Why it's done this way
Every stage pairs a generator with a judgment about it: an LCG with a period theorem, a shuffle with a uniformity proof, a statistic with a distribution. That pairing is the module's real lesson — Knuth's Chapter 3 is not a recipe box of generators, it is a training course in how to distrust one. The famous cautionary tale of §3.1 (his elaborate "super-random" Algorithm K that promptly collapsed into a 3178-cycle... or into a fixed point) opens the chapter for exactly this reason.
In the real world
RANDU is not a hypothetical: results computed with it in the 1960s–70s had to be re-examined once its 15-plane geometry surfaced, and it remains the canonical example of "passes 1-D tests, fails in 3-D". The naive shuffle's bias is a production bug class — a random-comparator shuffle famously skewed a 2010 browser-choice ballot — and your 27-tape enumeration in stage 3 is precisely the audit that catches it. Reservoir sampling runs in every stream-analytics stack that must sample fairly from data too big to hold. Chi-square is the same machinery behind A/B-test sanity checks: stage 2's function is a data-science primitive.
Proof techniques you practiced
- Number-theoretic structure — the full-period theorem ties a program's observable behavior to congruence conditions you can check by hand.
- Induction for uniformity — Algorithm P is proved fair by inducting on the suffix already shuffled.
- Divisibility obstruction — the naive shuffle cannot be fair because outcomes can't split evenly over 6 permutations; no amount of testing is as final as this one-line counting argument.
- Hypothesis testing done honestly — a statistic, its distribution, a significance threshold, and the discipline of not over-reading one run.
8. Where this leads
- The chi-square test is the entry point to §3.3's full battery (equi- distribution, gap, poker, coupon-collector, runs, serial correlation) and to §3.3.4's spectral test, the sharp tool that quantifies the RANDU flaw you saw crudely here.
- Full-period theory (§3.2.1.2) generalizes to combined and lagged
generators; the low-bit weakness motivates modern designs (xorshift, PCG,
counter-based) that this module's
Lcginterface can host unchanged. - Uniform shuffling and sampling underpin randomized algorithms throughout the course — quicksort's random pivots (Module 06), randomized data structures, and Monte-Carlo estimation.