0

I'm trying to write a RECURSIVE function that randomizes/shuffles an array.

The function I have written is using the Fisher-Yates shuffle method, which works fine on small arrays, but gives a 'Maximum call stack exceeded error' on my intended array containing 5000 elements

I wondered if someone could help me fix this method so that it still works recursively on larger arrays?

Here is the function below:

shuffleArray = (array, currentIndex=0) => {
    if (currentIndex >= array.length) {
      return array;
    }

    let randomIndex = Math.floor(Math.random() * currentIndex);

    let tempValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = tempValue;
    let newIndex = currentIndex += 1;
    return this.shuffleArray(array, newIndex);
  }

console.log(shuffleArray([1, 2, 3, 4, 5, 6]));
// returns a random array like [3,4,6,1,2,5]

console.log(shuffleArray([...Array(5000).keys()]));
// an example array of 5000 elements returns error: Uncaught RangeError: Maximum call stack size exceeded
George
  • 147
  • 2
  • 12
  • Not really possible, since most JavaScript engines don’t have proper tail calls. Unless you want to change it into some weird thing where each call shuffles half of the input, then the other half? (That will end up being “Fisher–Yates but we’re pretending it’s recursive”.) – Ry- Oct 31 '19 at 03:04
  • 1
    Also this implementation of the shuffle is wrong: it never keeps an element in the same place. – Ry- Oct 31 '19 at 03:22

2 Answers2

2

Any loop like this:

for (let i = 0; i < n; i++) {
    // do something with i
}

Can be pointlessly, but recursively, expanded into O(log n) space:

function foo(start, end) {
    if (start + 1 === end) {
        // do something with start
    } else {
        let mid = start + Math.floor((end - start) / 2);
        foo(start, mid);
        foo(mid, end);
    }
}

foo(0, n);
Ry-
  • 218,210
  • 55
  • 464
  • 476
  • How does this answer the OP's question? – Aadit M Shah Oct 31 '19 at 04:57
  • @AaditMShah: Implementing a shuffle in the form `for (let i = 0; i < n; i++) { … }` is left as an exercise to the reader. – Ry- Oct 31 '19 at 05:51
  • 1
    Ah, I see what's happening now. The max size of the call stack would be O(log n). It does indeed prevent stack overflows for quite large arrays. You would need to have arrays bigger than the amount of space you have available to be able to cause a stack overflow. Ingenious solution. +1 – Aadit M Shah Oct 31 '19 at 07:30
0

You could always use trampolining.

// data Trampoline a where

// Result :: a -> Trampoline a
const Result = result => ({ bounce: false, result });

// Bounce :: (a -> Trampoline b) -> a -> Trampoline b
const Bounce = func => (...args) => ({ bounce: true, func, args });

// trampoline :: Trampoline a -> a
const trampoline = t => {
    while (t.bounce) t = t.func(...t.args);
    return t.result;
};

// _shuffleArray :: ([a], Int) -> Trampoline [a]
const _shuffleArray = Bounce((array, currentIndex) => {
    if (currentIndex >= array.length) return Result(array);
    let randomIndex = Math.floor(Math.random() * currentIndex);
    [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
    return _shuffleArray(array, currentIndex + 1);
});

// shuffleArray :: [a] -> [a]
const shuffleArray = array => trampoline(_shuffleArray(array, 0));

// Shuffle 100k numbers without a stack overflow.
console.log(shuffleArray([...Array(1e5).keys()]));

This allows you to flatten any recursive function into an iterative loop.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299