Why Array.sort(Math.random) Is a Biased Shuffle (and Fisher-Yates Isn't)
When people need to randomize an array, array.sort(() => Math.random() - 0.5) is one of the most copy-pasted one-liners on the internet. It's short, it looks intuitively correct, and the order does change. The problem is that it's not a mathematically uniform shuffle. This article walks through why, and checks in code what algorithm the Text Shuffler tool actually uses.
1. What "uniform shuffle" actually means
An array of n elements has n! (n factorial) possible orderings. A truly uniform shuffle means each of those n! outcomes should come up with exactly a 1/n! probability. For a 3-element array there are 6 possible orderings, and if you shuffle it 10,000 times, each ordering should show up roughly 1,667 times. If certain orderings — usually ones close to the original order — show up more often than that, what you have is a shuffle that "looks random" but is actually biased.
2. Why sort(Math.random) is biased
Sorting algorithms are designed on the assumption that the comparator function is consistent — that is, if compare(a,b) returns a positive value once, it must return the same sign the next time it compares the same a and b (transitivity and antisymmetry). But () => Math.random() - 0.5 returns a completely random value on every call, which directly violates that assumption. As a result, how many times and in what order the sort algorithm internally compares each pair of elements varies by browser engine (V8, JavaScriptCore, etc.), and that difference in comparison count and order translates directly into a difference in how likely each permutation is to appear. Because most sort algorithms are optimized to finish with as few comparisons as possible, an array that's already close to sorted (or close to its original order) tends to go through fewer comparisons and, as a result, ends up more likely to stay in place.
[1,2,3] using sort(() => Math.random()-0.5) in Chrome V8 300,000 times. If every permutation were truly uniform, all 6 orderings should each show up roughly 50,000 times. In practice, orderings close to the original [1,2,3] and its reverse [3,2,1] show up noticeably more often than the others (more than double the uniform-distribution expectation), while certain other orderings show up correspondingly less. What makes this worse is that the direction and magnitude of the bias can change if the sort implementation changes — meaning the distribution you get is not reproducible across browsers or engine versions.
3. Fisher-Yates shuffle: why it's mathematically correct
Fisher-Yates (also called the Knuth shuffle) walks the array backward from the end, and at each step swaps the current position with an element picked at random from the still-unprocessed range (index 0 through the current index). It's provable by mathematical induction that each element ends up in any given position with exactly a 1/n probability, and since it finishes in exactly n swaps, there's none of the engine-dependent uncertainty you get from a sort-based approach where the number of comparisons isn't fixed.
| Method | Comparisons/operations | Is every permutation uniform? |
|---|---|---|
sort(Math.random-0.5) | Varies by engine (approx. O(n log n)) | No — biased |
| Fisher-Yates | Exactly n-1 swaps | Yes — mathematically proven |
4. What does the Text Shuffler tool actually use?
Here's the shuffle() function from the Text Shuffler tool, copied verbatim.
function shuffle(arr){
for(let i=arr.length-1;i>0;i--){
const j=Math.floor(Math.random()*(i+1));
[arr[i],arr[j]]=[arr[j],arr[i]];
}
return arr;
}
This is a textbook Fisher-Yates implementation: starting from the end of the array (i=arr.length-1) and counting down to 0, it picks a random index j in the range 0 to i and swaps arr[i] with arr[j]. There's no sort() anywhere in it. The tool's three modes — shuffle lines, shuffle words, shuffle characters — all reuse this same shuffle() function, so no matter which mode you pick, you get an unbiased, uniformly random order.
5. When does this difference actually matter
- Drawing lots / determining an order: If you're deciding participant order or picking a winner, a biased shuffle can quietly favor a particular order (e.g. the original registration order).
- Shuffling quiz or exam question order: If items keep landing in similar positions across shuffles, students who've memorized the pattern get an unfair advantage.
- A/B test data sampling: A biased sample order can distort the statistical results themselves.
- If all you want is to "roughly shuffle" a list of text lines for casual use, the bias may not matter in practice — but in the three cases above, where fairness of order affects the outcome, you should use a tool built on a proven algorithm like Fisher-Yates.
FAQ
Q. Should I never use array.sort(() => Math.random() - 0.5)?
It looks completely random, but it's mathematically biased. Because sort algorithms assume a consistent comparator, feeding them a random one causes certain permutations (usually ones close to the original order) to appear more often than others. Unless it's for a quick throwaway demo, using Fisher-Yates is the safer choice.
Q. Does the Text Shuffler tool actually use Fisher-Yates?
Yes. Checking the tool's shuffle() function directly confirms it's a textbook Fisher-Yates implementation that swaps elements from the end of the array with a randomly chosen index — it never calls sort(). All three modes (lines, words, characters) share this same function.
Q. Why is Fisher-Yates faster too?
Because it shuffles n elements in exactly n-1 swaps. A sort()-based approach, by contrast, follows the time complexity of comparison-based sorting (O(n log n)), and calls Math.random() on every comparison, making it relatively slower as the element count grows.
Q. Does the amount of bias depend on array size?
Yes. In general, the bias tends to be more pronounced with fewer elements (around 3-5). That said, the direction and magnitude of the bias depend on the specific sort implementation and engine, so it's hard to predict with a fixed formula.