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;
}
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];
}
}
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);
}
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)