DEV Community

Michael Lip
Michael Lip

Posted on • Originally published at zovo.one

Fair Random Selection: Why Shuffling a List Wrong Gives Biased Results

Pick a random item from a list. Sounds trivial. And for a single selection it is: generate a random index from 0 to length-1, return that item. But the moment you need to select multiple items, ensure fairness, or shuffle a list, the naive approach introduces bias.

The Fisher-Yates shuffle

The correct algorithm for shuffling a list is Fisher-Yates (also called Knuth shuffle). It works backward through the array, swapping each element with a randomly selected element from the remaining unshuffled portion:

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
  return array;
}
Enter fullscreen mode Exit fullscreen mode

This produces every possible permutation with equal probability. The key is Math.random() * (i + 1), which selects from 0 to i (inclusive). Using Math.random() * array.length instead (selecting from the entire array on every iteration) produces biased results.

Proving the bias

Consider shuffling [1, 2, 3]. With the correct algorithm, there are 3! = 6 equally probable permutations. With the broken algorithm (selecting from the full range on every step), there are 3^3 = 27 possible execution paths mapping to 6 permutations. 27 doesn't divide evenly by 6, so some permutations are more likely than others.

Running the broken shuffle a million times and counting the frequency of each permutation reveals the bias clearly. Some orderings appear about 20% more often than others. For a random picker choosing a raffle winner, this means certain positions in the original list have a systematically higher chance of being selected.

Weighted random selection

Sometimes items shouldn't have equal probability. In a weighted raffle, someone who bought 5 tickets should have 5x the chance of someone who bought 1.

The standard approach builds a cumulative weight array and uses binary search:

function weightedPick(items, weights) {
  const cumulative = [];
  let total = 0;
  for (const w of weights) {
    total += w;
    cumulative.push(total);
  }
  const r = Math.random() * total;
  for (let i = 0; i < cumulative.length; i++) {
    if (r < cumulative[i]) return items[i];
  }
}
Enter fullscreen mode Exit fullscreen mode

Sampling without replacement

Picking k items from n without repeats is a common need: lottery numbers, random team assignments, raffle winners. The naive approach (keep picking randomly and retry on duplicates) works but gets slow as k approaches n.

The better approach is a partial Fisher-Yates: shuffle the first k elements and take them. This runs in O(k) time regardless of n:

function sample(array, k) {
  const result = [...array];
  for (let i = 0; i < k; i++) {
    const j = i + Math.floor(Math.random() * (result.length - i));
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result.slice(0, k);
}
Enter fullscreen mode Exit fullscreen mode

I built a random picker at zovo.one/free-tools/random-picker that handles single picks, multiple picks without replacement, weighted selection, and group assignment. It uses Fisher-Yates internally, so the results are mathematically fair.

I'm Michael Lip. I build free developer tools at zovo.one. 500+ tools, all private, all free.

Top comments (0)