fix(permutations-heap): do the swaps on a copy of the input

Heap's Algorithm generates each permutation by swapping two elements of a
working array in place, and that working array was the argument itself, so
permutationsHeap([1, 2, 3]) left the caller holding [3, 2, 1]. Two calls on
one array returned different lists, and a frozen array threw.

Copy the input up front and swap on the copy, as shuffle and numericSort
already do. The returned permutations are unchanged.
This commit is contained in:
dualfroz committed 2026-09-03 14:06:28 +02:00
1 parent cd7bef4165
commit 5894b8b560
3 files changed
+35 -8

No files matched your search

@@ -0,0 +1,5 @@
---
"simple-statistics": patch
---
Fix `permutationsHeap` permuting the caller's array. Heap's Algorithm works by swapping elements in place, and the implementation ran those swaps directly on the argument, so `permutationsHeap([1, 2, 3])` left the caller holding `[3, 2, 1]`, the last permutation generated. Calling it twice on the same array returned two different lists, and a frozen input threw `TypeError: Cannot assign to read only property '0'`. The swaps now run on a copy, matching the defensive copying already used in `shuffle`. The returned permutations are unchanged.
+15 -8
View File
@@ -2,18 +2,25 @@
* Implementation of [Heap's Algorithm](https://en.wikipedia.org/wiki/Heap%27s_algorithm) * Implementation of [Heap's Algorithm](https://en.wikipedia.org/wiki/Heap%27s_algorithm)
* for generating permutations. * for generating permutations.
* *
* Heap's Algorithm permutes an array in place. This function does the swapping
* on a copy, so it will not modify its input.
*
* @param {Array} elements any type of data * @param {Array} elements any type of data
* @returns {Array<Array>} array of permutations * @returns {Array<Array>} array of permutations
*/ */
function permutationsHeap(elements) { function permutationsHeap(elements) {
const indexes = new Array(elements.length); // slice the original array so that it is not modified: the swaps below
const permutations = [elements.slice()]; // are done in place
const sample = elements.slice();
for (let i = 0; i < elements.length; i++) { const indexes = new Array(sample.length);
const permutations = [sample.slice()];
for (let i = 0; i < sample.length; i++) {
indexes[i] = 0; indexes[i] = 0;
} }
for (let i = 0; i < elements.length; ) { for (let i = 0; i < sample.length; ) {
if (indexes[i] < i) { if (indexes[i] < i) {
// At odd indexes, swap from indexes[i] instead // At odd indexes, swap from indexes[i] instead
// of from the beginning of the array // of from the beginning of the array
@@ -24,11 +31,11 @@ function permutationsHeap(elements) {
// swap between swapFrom and i, using // swap between swapFrom and i, using
// a temporary variable as storage. // a temporary variable as storage.
const temp = elements[swapFrom]; const temp = sample[swapFrom];
elements[swapFrom] = elements[i]; sample[swapFrom] = sample[i];
elements[i] = temp; sample[i] = temp;
permutations.push(elements.slice()); permutations.push(sample.slice());
indexes[i]++; indexes[i]++;
i = 0; i = 0;
} else { } else {
+15
View File
@@ -16,4 +16,19 @@ describe("permutationsHeap", function () {
[3, 2, 1] [3, 2, 1]
]); ]);
}); });
it("does not modify the caller's array", function () {
const x = [1, 2, 3];
ss.permutationsHeap(x);
assert.deepEqual(x, [1, 2, 3]);
});
it("accepts a frozen array", function () {
assert.deepEqual(ss.permutationsHeap(Object.freeze([1, 2])), [
[1, 2],
[2, 1]
]);
});
it("returns the same permutations when called twice on one array", function () {
const x = [1, 2, 3];
assert.deepEqual(ss.permutationsHeap(x), ss.permutationsHeap(x));
});
}); });