From 8c45a61186f3096288157fb8aaf6a28c62015fa0 Mon Sep 17 00:00:00 2001 From: Shaban-Eissa Date: Tue, 28 May 2024 21:02:27 +0300 Subject: [PATCH] feat: adding details for shuffle function --- 8.can-you-shuffle-an-array.md | 257 ++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 8.can-you-shuffle-an-array.md diff --git a/8.can-you-shuffle-an-array.md b/8.can-you-shuffle-an-array.md new file mode 100644 index 0000000..a7acadf --- /dev/null +++ b/8.can-you-shuffle-an-array.md @@ -0,0 +1,257 @@ +# 8. can you shuffle() an array? + +### Problem + +https://bigfrontend.dev/problem/can-you-shuffle-an-array + +# + +### Problem Description + +How would you implement a `shuffle()` ? + +When passed with an array, it should modify the array inline to generate a randomly picked permutation at the same probability. + +for an array like this: + +```js +const arr = [1, 2, 3, 4]; +``` + +there would be possibly 4! = 24 permutations + +``` +[1, 2, 3, 4] +[1, 2, 4, 3] +[1, 3, 2, 4] +[1, 3, 4, 2] +[1, 4, 2, 3] +[1, 4, 3, 2] +[2, 1, 3, 4] +[2, 1, 4, 3] +[2, 3, 1, 4] +[2, 3, 4, 1] +[2, 4, 1, 3] +[2, 4, 3, 1] +[3, 1, 2, 4] +[3, 1, 4, 2] +[3, 2, 1, 4] +[3, 2, 4, 1] +[3, 4, 1, 2] +[3, 4, 2, 1] +[4, 1, 2, 3] +[4, 1, 3, 2] +[4, 2, 1, 3] +[4, 2, 3, 1] +[4, 3, 1, 2] +[4, 3, 2, 1] +``` + +your `shuffle()` should transform the array in one of the above array, at the same 1/24 probability. + +**notes** + +Your `shuffle()` will be called multiple times, to calculate the probability on each possible result, and test again [standard deviation](https://simple.wikipedia.org/wiki/Standard_deviation) + +ref: https://javascript.info/task/shuffle + +# + +### Solution + +```js +/** + * @param {any[]} arr + */ +function shuffle(array) { + for (let i = array.length - 1; i > 0; i--) { + let j = Math.floor(Math.random() * (i + 1)); // random index from 0 to i + + // swap elements array[i] and array[j] + // we use "destructuring assignment" syntax to achieve that + // same can be written as: + // let t = array[i]; array[i] = array[j]; array[j] = t + [array[i], array[j]] = [array[j], array[i]]; + } +} +``` + +# + +### Explanation +Multiple runs of shuffle may lead to different orders of elements. For instance: + +```js +let arr = [1, 2, 3]; + +shuffle(arr); +// arr = [3, 2, 1] + +shuffle(arr); +// arr = [2, 1, 3] + +shuffle(arr); +// arr = [3, 1, 2] +
+ +shuffle(arr); +// ... +``` + +```js +function shuffle(array) { + array.sort(() => Math.random() - 0.5); +} + +let arr = [1, 2, 3]; +shuffle(arr); +``` +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. + +But because the sorting function is not meant to be used this way, not all permutations have the same probability. + +For instance, consider the code below. It runs shuffle 1000000 times and counts appearances of all possible results: +```js +function shuffle(array) { + array.sort(() => Math.random() - 0.5); +} + +// counts of appearances for all possible permutations +let count = { + '123': 0, + '132': 0, + '213': 0, + '231': 0, + '321': 0, + '312': 0 +}; + +for (let i = 0; i < 1000000; i++) { + let array = [1, 2, 3]; + shuffle(array); + count[array.join('')]++; +} + +// show counts of all possible permutations +for (let key in count) { + alert(`${key}: ${count[key]}`); +} +``` +An example result (depends on JS engine): +``` +123: 250706 +132: 124425 +213: 249618 +231: 124880 +312: 125148 +321: 125223 +``` + +let's break this down: + +1. **Define the `shuffle` function**: This function takes an array as an argument and sorts it using a comparator function that returns a random number between -0.5 and 0.5. This effectively shuffles the array, but it doesn't generate a truly random shuffle because not all permutations are equally likely. + +2. **Initialize `count` object**: This object will keep track of the number of times each possible permutation of the array [1, 2, 3] appears when the array is shuffled. + +3. **Run the shuffle function multiple times**: The code that follows (which is not shown in the excerpt) would run the `shuffle` function a large number of times (e.g., 1,000,000 times) and increment the count of the resulting permutation each time. + +4. **Analyze the results**: By looking at the counts of each permutation, you can see that some permutations appear more often than others. This demonstrates that the `shuffle` function does not generate a truly random shuffle, because in a truly random shuffle, all permutations would be equally likely. + +The reason for this bias is that the `Array.sort()` method is not designed to be used with a comparator function that returns a random value. The sort order of two elements should be deterministic and based on the values of the elements themselves, not on a random number. When you use a random comparator function, the behavior of `Array.sort()` is not well-defined, and it can lead to biased results. + + +There are other good ways to do the task. For instance, there’s a great algorithm called Fisher-Yates shuffle. The idea is to walk the array in the reverse order and swap each element with a random one before it: + +```js +function shuffle(array) { + for (let i = array.length - 1; i > 0; i--) { + let j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } +} + +// counts of appearances for all possible permutations +let count = { + '123': 0, + '132': 0, + '213': 0, + '231': 0, + '321': 0, + '312': 0 +}; + +for (let i = 0; i < 1000000; i++) { + let array = [1, 2, 3]; + shuffle(array); + count[array.join('')]++; +} + +// show counts of all possible permutations +for (let key in count) { + alert(`${key}: ${count[key]}`); +} +``` + +The example output: +``` +123: 166693 +132: 166647 +213: 166628 +231: 167517 +312: 166199 +321: 166316 +``` + +Looks good now: all permutations appear with the same probability. + +Also, performance-wise the Fisher-Yates algorithm is much better, there’s no “sorting” overhead. + + + +# + +### Usage + +```js +function generateRandomArray(n) { + const array = Array.from({ length: n }, (_, i) => i); + shuffle(array); + return array; +} + +const array = generateRandomArray(10); +console.log(array); // [5, 7, 2, 4, 8, 6, 1, 9, 3, 0] +console.log(array); // [0, 5, 4, 9, 7, 8, 1, 2, 3, 6] +console.log(array); // [2, 1, 4, 0, 3, 5, 6, 7, 8, 9] +``` + +# + +### Real World Examples + +1. Randomizing Quiz Questions - Use Case **Present questions in a random order.** +```js +function shuffle(array) { /*...*/ } +const questions = ['Q1', 'Q2', 'Q3', 'Q4']; +const shuffledQuestions = shuffle(questions); +``` +2. Shuffling Playlist Songs - Use Case **Play songs in a random order.** +```js +function shuffle(array) { /*...*/ } +const playlist = ['song1.mp3', 'song2.mp3', 'song3.mp3']; +const shuffledPlaylist = shuffle(playlist); +``` +3. Randomizing Survey Responses - Use Case **Minimize order bias in survey options.** +```js +function shuffle(array) { /*...*/ } +const responses = ['Strongly Agree', 'Agree', 'Neutral', 'Disagree', 'Strongly Disagree']; +const shuffledResponses = shuffle(responses); +``` + +# + +### Reference + +[Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) +
+[Shuffle under-the-hood](https://javascript.info/task/shuffle)