PickRandom LogoPickRandom

Learn

The Fisher–Yates Shuffle — and Why Naive Shuffles Are Biased

6 min read

Shuffling looks like the easiest problem in randomness: mix a list until the order is unpredictable. Yet the most popular one-liner for it — sorting with a random comparator — is quietly broken, and the correct algorithm is barely longer. This article covers both: why the shortcut is biased, and how Fisher–Yates gets it exactly right.

The tempting one-liner

You have almost certainly seen this:

// Looks random. Isn't fair.
const shuffled = items.sort(() => Math.random() - 0.5);

The idea seems sound — answer “which of these two comes first?” with a coin flip and let the sort do the rest. The flaw is that sorting algorithms are built on a promise the coin flip breaks: the comparator must be consistent. If a < b now, it must be a < b every time, and if a < b and b < c, then a < c. A random comparator violates all of that, so the sort wanders through its comparisons in whatever pattern its internals dictate — and different engines use different sorts (V8 uses Timsort), so the bias even changes between browsers.

And it is measurably biased. Run the one-liner a million times on [1, 2, 3] and count the outcomes: some orderings show up noticeably more often than the one-in-six you’d expect, with elements tending to linger near their starting positions. For a party trick that’s harmless. For assigning teams, drawing raffle order, or dealing cards, it means some outcomes are systematically favored — the definition of an unfair shuffle. In the worst case, an inconsistent comparator can even leave some engines’ sorts with undefined behavior.

Fisher–Yates: the correct three lines

The fix dates to 1938, when Ronald Fisher and Frank Yates described it as a pencil-and-paper procedure; Richard Durstenfeld adapted it for computers in 1964. The modern form walks the array from the end, swapping each position with a uniformly random position at or before it:

function shuffle(arr) {
  for (let i = arr.length - 1; i > 0; i--) {
    const j = randomInt(0, i); // uniform integer in [0, i] — inclusive!
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
  return arr;
}

Why is this fair? Count the choices. The first iteration picks j from n options, the next from n − 1, then n − 2, and so on: n × (n − 1) × … × 2 × 1 = n! equally likely paths through the loop. An array of n items has exactly n! possible orderings, and each path lands on a different one — a perfect one-to-one match. Every permutation therefore occurs with probability exactly 1/n!. No ordering is favored, no element “prefers” its starting position, and the whole thing runs in a single O(n) pass.

The classic off-by-one trap

Fisher–Yates has one famous way to get it subtly wrong: picking j from the whole array every time instead of the shrinking prefix. That version makes n choices of n options — nn paths. But nn is not divisible by n! (for n > 2), so the paths cannot spread evenly across the n! permutations; some orderings necessarily receive more paths than others. For three items, 27 paths must cover 6 permutations — 27 ÷ 6 doesn’t divide, so the shuffle is provably biased before you run it once. The inclusive-bound detail in the loop is load-bearing.

The randomness underneath still matters

Fisher–Yates is only as good as the random numbers feeding it. Two details finish the job:

  • Uniform integers — derive j with rejection sampling rather than Math.floor(random * (i + 1)) on a biased source; any lean in j leans the whole shuffle
  • An unpredictable generator — a shuffle built on a predictable PRNG can be fair in distribution yet still foreseeable; with a seeded Math.random(), anyone who recovers the state can replay your "random" deal
  • Enough state — a PRNG with too little internal state cannot even reach every permutation of a large list: 52! orderings of a card deck need ~226 bits, more than many PRNGs hold

That is why PickRandom’s shuffling tools pair Fisher–Yates with crypto.getRandomValues() — a fair algorithm on top of an unpredictable source. When the Team Splitter deals people into groups or the list picker draws an order, every arrangement is exactly as likely as every other, and nobody can compute the result before the draw. Three lines of loop, one good source of bits: that is the entire recipe for an honest shuffle.

Try it yourself

  • Team Splitter Randomly divide / split people into balanced teams. Ideal for sports, group projects, or game nights.
  • Random from List Simple random selection from a list of options. Fast and straightforward for quick decisions.

← Back to all guides