diff --git a/package.json b/package.json index 9971ed6..be3fe94 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@aureooms/js-random", "description": "Randomness algorithms for JavaScript", - "version": "3.2.0", + "version": "3.2.1", "license": "AGPL-3.0", "author": "Aurélien Ooms ", "homepage": "https://aureooms.github.io/js-random", diff --git a/src/api/sample.js b/src/api/sample.js index 764a5aa..2a59cdc 100644 --- a/src/api/sample.js +++ b/src/api/sample.js @@ -1,5 +1,16 @@ import _fisheryates from '../kernel/_fisheryates.js'; import randint from './randint.js'; +/** + * Take a sample of size n (without repetitions) from the items i through + * j-1 of the input array. This is done in-place. The sample can be retrieved + * from position i to i+n. + * + * @function + * @param {number} n The size of the sample. + * @param {Array} a The input array. + * @param {number} i The inclusive left bound. + * @param {number} j The non-inclusive right bound. + */ const sample = _fisheryates(randint); export default sample; diff --git a/src/api/shuffle.js b/src/api/shuffle.js index fba3121..6ef786b 100644 --- a/src/api/shuffle.js +++ b/src/api/shuffle.js @@ -1,5 +1,13 @@ import _shuffle from '../kernel/_shuffle.js'; import sample from './sample.js'; +/** + * Shuffle array in-place between positions i and j-1 (inclusive). + * The other positions are left untouched. + * @function + * @param {Array} a The input array. + * @param {number} i The inclusive left bound. + * @param {number} j The non-inclusive right bound. + */ const shuffle = _shuffle(sample); export default shuffle; diff --git a/src/kernel/_fisheryates.js b/src/kernel/_fisheryates.js index 9f58486..b981728 100644 --- a/src/kernel/_fisheryates.js +++ b/src/kernel/_fisheryates.js @@ -1,23 +1,59 @@ /** - * Sample array using Fisher-Yates method. + * Sample element from an array using Fisher-Yates method. + * + * NOTE: The original description of the algorithm by Fisher and Yates [1] had + * unnecessary bookkeeping which made the algorithm run in O(n * (j-i)) time. + * This implementation follows Durstenfeld's [2] and Knuth's [3] descriptions + * which yield O(n) running time. + * + * For more information see the excellent "Fisher–Yates shuffle" page on + * Wikipedia [4]. Fisher and Yates description is referred there as + * "Fisher and Yate's original method" or "pencil-and-paper method". + * The more efficient implementation described by Durstenfeld and Knuth is + * referred there as "the modern method". + * + * [1] Fisher, Ronald A.; Yates, Frank (1938). Statistical tables for + * biological, agricultural and medical research. + * [2] Durstenfeld, R. (July 1964). "Algorithm 235: Random permutation" + * [3] Knuth, Donald E. (1969). Seminumerical algorithms. The Art of Computer + * Programming Volume 2. + * [4] https://en.wikipedia.org/wiki/Fisher–Yates_shuffle + * + * @param {Function} randint The randint function. + * @return {Function} The sampling function. */ +const _fisheryates = (randint) => { + /** + * Take a sample of size n (without repetitions) from the items i through + * j-1 of the input array. This is done in-place. The sample can be + * retrieved from position i to i+n. + * + * @param {number} n The size of the sample. + * @param {Array} a The input array. + * @param {number} i The inclusive left bound. + * @param {number} j The non-inclusive right bound. + */ + const sample = (n, a, i, j) => { + // We will swap at most n elements + // NOTE: When n = j - i, the last swap swaps a[j-1] with itself, + // which is a NOOP. /!\ HOWEVER, the last swap is NOT a NOOP when + // n < j - i. Hence we cannot let k = i + n - 1 in general. + const k = i + n; -const _fisheryates = (randint) => (n, a, i, j) => { - // We will swap at most n elements + for (; i < k; ++i) { + // Choose any index p in the remaining array - const k = i + n; + const p = randint(i, j); - for (; i < k; ++i) { - // Choose any index p in the remaining array + // Swap element at index p with first element in the array - const p = randint(i, j); + const tmp = a[i]; + a[i] = a[p]; + a[p] = tmp; + } + }; - // Swap element at index p with first element in the array - - const tmp = a[i]; - a[i] = a[p]; - a[p] = tmp; - } + return sample; }; export default _fisheryates; diff --git a/src/kernel/_shuffle.js b/src/kernel/_shuffle.js index 121a06c..3921806 100644 --- a/src/kernel/_shuffle.js +++ b/src/kernel/_shuffle.js @@ -1,6 +1,20 @@ /** - * Shuffle array by sampling the complete array. + * Shuffle the array by sampling the entire array. + * + * @param {Function} sample The sample function. + * @return {Function} The shuffle function. */ +const _shuffle = (sample) => { + /** + * Shuffle array in-place between positions i and j-1 (inclusive). + * The other positions are left untouched. + * + * @param {Array} a The input array. + * @param {number} i The inclusive left bound. + * @param {number} j The non-inclusive right bound. + */ + const shuffle = (a, i, j) => sample(j - i, a, i, j); + return shuffle; +}; -const _shuffle = (sample) => (a, i, j) => sample(j - i, a, i, j); export default _shuffle;