|
| 1 | +The simple solution could be: |
| 2 | + |
| 3 | +```js run |
| 4 | +*!* |
| 5 | +function shuffle(array) { |
| 6 | + array.sort(() => Math.random() - 0.5); |
| 7 | +} |
| 8 | +*/!* |
| 9 | + |
| 10 | +let arr = [1, 2, 3]; |
| 11 | +shuffle(arr); |
| 12 | +alert(arr); |
| 13 | +``` |
| 14 | + |
| 15 | +That somewhat works, because `Math.random()-0.5` is a random number that may be positive or negative, so the sorting function reorders elements randomly. |
| 16 | + |
| 17 | +But because the sorting function is not meant to be used this way, not all permutations have the same probability. |
| 18 | + |
| 19 | +For instance, consider the code below. It runs `shuffle` 1000000 times and counts appearances of all possible results: |
| 20 | + |
| 21 | +```js run |
| 22 | +function shuffle(array) { |
| 23 | + array.sort(() => Math.random() - 0.5); |
| 24 | +} |
| 25 | + |
| 26 | +// counts of appearances for all possible permutations |
| 27 | +let count = { |
| 28 | + '123': 0, |
| 29 | + '132': 0, |
| 30 | + '213': 0, |
| 31 | + '231': 0, |
| 32 | + '321': 0, |
| 33 | + '312': 0 |
| 34 | +}; |
| 35 | + |
| 36 | +for(let i = 0; i < 1000000; i++) { |
| 37 | + let array = [1, 2, 3]; |
| 38 | + shuffle(array); |
| 39 | + count[array.join('')]++; |
| 40 | +} |
| 41 | + |
| 42 | +// show counts of all possible permutations |
| 43 | +for(let key in count) { |
| 44 | + alert(`${key}: ${count[key]}`); |
| 45 | +} |
| 46 | +``` |
| 47 | + |
| 48 | +An example result (for V8, July 2017): |
| 49 | + |
| 50 | +```js |
| 51 | +123: 250706 |
| 52 | +132: 124425 |
| 53 | +213: 249618 |
| 54 | +231: 124880 |
| 55 | +312: 125148 |
| 56 | +321: 125223 |
| 57 | +``` |
| 58 | + |
| 59 | +We can see the bias clearly: `123` and `213` appear much more often than others. |
| 60 | + |
| 61 | +The result of the code may vary between JavaScript engines, but we can already see that the approach is unreliable. |
| 62 | + |
| 63 | +Why it doesn't work? Generally speaking, `sort` is a "black box": we throw an array and a comparison function into it and expect the array to be sorted. But due to the utter randomness of the comparison the black box goes mad, and how exactly it goes mad depends on the concrete implementation that differs between engines. |
| 64 | + |
| 65 | +There are other good ways to do the task. For instance, there's a great algorithm called [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle). The idea is to walk the array in the reverse order and swap each element with a random one before it: |
| 66 | + |
| 67 | +```js |
| 68 | +function shuffle(array) { |
| 69 | + for(let i = array.length - 1; i > 0; i--) { |
| 70 | + let j = Math.floor(Math.random() * (i+1)); // random index from 0 to i |
| 71 | + [array[i], array[j]] = [array[j], array[i]]; // swap elements |
| 72 | + } |
| 73 | +} |
| 74 | +``` |
| 75 | + |
| 76 | +Let's test it the same way: |
| 77 | + |
| 78 | +```js run |
| 79 | +function shuffle(array) { |
| 80 | + for(let i = array.length - 1; i > 0; i--) { |
| 81 | + let j = Math.floor(Math.random() * (i+1)); |
| 82 | + [array[i], array[j]] = [array[j], array[i]]; |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +// counts of appearances for all possible permutations |
| 87 | +let count = { |
| 88 | + '123': 0, |
| 89 | + '132': 0, |
| 90 | + '213': 0, |
| 91 | + '231': 0, |
| 92 | + '321': 0, |
| 93 | + '312': 0 |
| 94 | +}; |
| 95 | + |
| 96 | +for(let i = 0; i < 1000000; i++) { |
| 97 | + let array = [1, 2, 3]; |
| 98 | + shuffle(array); |
| 99 | + count[array.join('')]++; |
| 100 | +} |
| 101 | + |
| 102 | +// show counts of all possible permutations |
| 103 | +for(let key in count) { |
| 104 | + alert(`${key}: ${count[key]}`); |
| 105 | +} |
| 106 | +``` |
| 107 | + |
| 108 | +The example output: |
| 109 | + |
| 110 | +```js |
| 111 | +123: 166693 |
| 112 | +132: 166647 |
| 113 | +213: 166628 |
| 114 | +231: 167517 |
| 115 | +312: 166199 |
| 116 | +321: 166316 |
| 117 | +``` |
| 118 | + |
| 119 | +Looks good now: all permutations appear with the same probability. |
| 120 | + |
| 121 | +Also, performance-wise the Fisher-Yates algorithm is much better, there's no "sorting" overhead. |
0 commit comments