Why does this simple shuffle algorithm produce biased results?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
The naive shuffle algorithm — swapping each element with a randomly chosen element from the entire array — produces biased results because it generates n^n possible swap sequences for n elements, but there are only n! permutations. Since n^n is not evenly divisible by n! (for n > 2), some permutations are produced more often than others. The correct algorithm is the Fisher-Yates (Knuth) shuffle, which restricts each swap to the remaining unshuffled portion of the array, generating exactly n! equally likely outcomes.
The Naive (Biased) Shuffle
For an array of 3 elements, this algorithm has 3^3 = 27 possible outcomes (3 choices at each of 3 steps). But there are only 3! = 6 permutations. Since 27 is not divisible by 6 evenly (27 / 6 = 4.5), some permutations must appear more frequently than others.
Demonstrating the Bias
Running this with 1 million trials reveals significant deviation from the expected uniform distribution.
Why n^n Cannot Be Uniform Over n!
For n = 3:
- Naive shuffle: 3^3 = 27 equally likely swap sequences
- Permutations: 3! = 6
- 27 / 6 = 4.5 — not an integer
Since 27 sequences cannot be divided evenly into 6 groups, at least one permutation must be overrepresented. Specifically:
- 3 permutations appear in 5 out of 27 sequences (18.5%)
- 3 permutations appear in 4 out of 27 sequences (14.8%)
For larger arrays, the bias compounds. With n = 52 (a deck of cards), 52^52 possible outcomes mapped to 52! permutations produces extreme bias.
The Fisher-Yates Shuffle (Correct)
Fisher-Yates generates exactly n! outcomes: at step 1 there are n choices, at step 2 there are n-1 choices, and so on. The product n * (n-1) * ... * 1 = n!, and each path maps to exactly one permutation.
Verifying Fisher-Yates Is Unbiased
Python's Built-in Shuffle
Python's random.shuffle() implements Fisher-Yates internally. Always use the standard library function rather than writing your own.
Other Incorrect Shuffle Variants
Implementation in Other Languages
Common Pitfalls
- Using
random.randint(0, n-1)instead ofrandom.randint(0, i): The key difference between the naive and Fisher-Yates shuffles is the range of the random index. Picking from the entire array (0ton-1) at every step causes bias. Picking from the remaining unshuffled range (0toioriton-1) produces uniform results. - Off-by-one errors in the Fisher-Yates loop: The backward variant loops from
n-1down to1(not0), pickingjin[0, i]. The forward variant loops from0ton-2, pickingjin[i, n-1]. Including or excluding the wrong boundary shifts the distribution. - Using
Math.random() * nwithoutMath.floor()in JavaScript:Math.random()returns a float in[0, 1). WithoutMath.floor(), you get a float index that JavaScript silently converts, potentially causing incorrect behavior. - Assuming sort-based shuffling is correct: Sorting with random keys (
arr.sort(() => Math.random() - 0.5)) is a common pattern in JavaScript tutorials, but the distribution depends on the sort algorithm's comparison patterns and is not guaranteed to be uniform. - Not understanding that bias matters in practice: For a deck of 52 cards, the naive shuffle favors certain orderings over others. In games, gambling, cryptographic applications, or scientific simulations, this bias can be exploited or produce invalid results.
Summary
- The naive shuffle (swap with any random index) is biased because n^n swap sequences cannot map uniformly to n! permutations
- Fisher-Yates (Knuth) shuffle is correct: at each step, swap with a random element from the unshuffled portion only
- The total number of Fisher-Yates outcomes is n * (n-1) * ... * 1 = n!, with each permutation appearing exactly once
- Use standard library functions (
random.shufflein Python,Collections.shufflein Java) — they implement Fisher-Yates - Sort-based shuffling and adjacent-swap methods are also biased or incomplete — avoid them

