10

Can anyone show me an iterative solution for the following problem? I solved it recursively but struggled with an iterative solution. (Facebook Technical Interview Question)

Input: [1, {a: 2}, [3], [[4, 5], 6], 7]
Output: [1, {a: 2}, 3, 4, 5, 6, 7]

Solution must work with n-th nested array elements (i.e. it must still work if someone modifies the array values/placement in the example above)

Recursive solution:

var flatten = function(input) {
    var result = [];

    input.forEach(function(element) {
        result = result.concat(Array.isArray(element) ? flatten(element) : element);
    });

    return result;
}
subject117
  • 103
  • 1
  • 6
  • possible duplicate of [Design patterns for converting recursive algorithms to iterative ones](http://stackoverflow.com/questions/1549943/design-patterns-for-converting-recursive-algorithms-to-iterative-ones) – Ted Hopp May 01 '15 at 16:38
  • Is this homework? What have you tried for the iterative solution? Why are you pursuing the iterative solution if you already have a recursive solution? – jfriend00 May 01 '15 at 16:40
  • Because the technical interviewer wanted to see an iterative solution after I showed him the recursive one (to test my technical skills I assume). – subject117 May 01 '15 at 16:45
  • For this specific problem, the easiest way is to just do one level at a time, and repeat until there's nothing more to do. (That has higher algorithmic complexity, though, than maintaining a stack of remaining work.) – ruakh May 01 '15 at 16:45
  • 1
    Just out of curiosity, what's supposed to happen with an input of `[1, {a: [2, [3, 4]]}]`? Is the output supposed to be the same as the input or `[1, {a: [2, 3, 4]}]` (or something else)? In other words, is the flattening process supposed to reach inside objects or just flatten elements that are themselves arrays? – Ted Hopp May 01 '15 at 16:53
  • It was just a quick technical phone screening so the input value was kept relatively simple due to time constraints. In this case, I didn't have to account for complexity in the object value. (The problem is referenced in an even simpler form for Facebook interviews on glassdoor). – subject117 May 01 '15 at 17:04
  • I attempted to do an in-depth analysis of this problem here if anyone is interested. https://gist.github.com/jcarroll2007/4ee72b3e99507c4f8ce3916fca147ab7 – Jordan Carroll Oct 25 '18 at 13:56

11 Answers11

11

Here is one way:

var input = [1, {a: 2}, [3], [[4, 5], 6], 7];
function flatten(input) {
    var i, placeHolder = [input], lastIndex = [-1], out = [];
    while (placeHolder.length) {
        input = placeHolder.pop();
        i = lastIndex.pop() + 1;
        for (; i < input.length; ++i) {
            if (Array.isArray(input[i])) {
                placeHolder.push(input);
                lastIndex.push(i);
                input = input[i];
                i = -1;
            } else out.push(input[i]);
        }
    }
    return out;
}
flatten(input);

Explanation: If iterating over a nested structure, you just have to remember where you were before by saving the current array and position before moving into the nested array (this is usually taken care of via the stack for recursive solutions).

Note: If you reuse the arrays placeHolder and lastIndex you won't need to keep recreating them every time. Perhaps something like this:

var flatten = function(){ 
    var placeHolder = [], lastIndex = [];
    placeHolder.count = 0;
    lastIndex.count = 0;
    return function flatten(input) {
        var i, out = [];
        placeHolder[0] = input; placeHolder.count = 1;
        lastIndex[0] = -1; lastIndex.count = 1;
        while (placeHolder.count) {
            input = placeHolder[--placeHolder.count];
            i = lastIndex[--lastIndex.count] + 1;
            for (; i < input.length; ++i) {
                if (Array.isArray(input[i])) {
                    placeHolder[placeHolder.count++] = input;
                    lastIndex[lastIndex.count++] = i;
                    input = input[i];
                    i = -1;
                } else out.push(input[i]);
            }
        }
        return out;
    }
}();

This is even faster again (for flat iteration that is), and less garbage collector issues calling it many times. The speed is very close to that of recursive function calling in Chrome, and many times faster than recursion in FireFox and IE.

I recreated Tomalak's tests here since the old jsPerf is broken for editing: https://jsperf.com/iterative-array-flatten-2

James Wilkins
  • 6,836
  • 3
  • 48
  • 73
  • So the problem with the recursive solution is that you run out of stack space for arbitrarily nested arrays. With this solution, it chokes on the garbage collection. – U Avalos Jul 19 '16 at 02:54
  • Note sure how that is. There's no allocation within the loop, except not sure if pre-allocating the array length might help. Also, keep in mind this was a simple answer for the OP trying the find "some way" and his is "some way", though perhaps not the best way, but the goal is not for a game engine, so take it easy. ;) In fact, I was trying to be more illustrative for teaching purposes. – James Wilkins Jul 20 '16 at 05:41
  • After some thought it very might well be that increasing the array lengths might cause new array allocations in some browser implementations. If my guess is correct, simply preallocating the arrays, and caching them, might help. – James Wilkins Jul 20 '16 at 05:57
  • according to [my benchmarks](https://codepen.io/mindplay-dk/pen/EbeVGP) this is not very fast - but should have much better memory characteristics than the fastest solution I've found so far. – mindplay.dk Nov 27 '17 at 15:04
  • I imagine it was fastest as proven back in 2015 (ignoring ES6 to allow things to work in IE also, and Rick Edwards just posted 19 days ago), but given browser optimizations, etc., over the years things are bound to change - especially if native calls are used, but (as you pointed out) not necessarily better in memory management for very large nested arrays. If I needed something similar for use in a game engine, I know which one I'd choose. ;) (in fact, this is actually what I use for a WebGL based simulation graph tree in order to flatten the hierarchy and cache it with very low GC overhead) – James Wilkins Nov 29 '17 at 02:47
  • How do we reason about run time complexity of this solution using Big O? Can we say O(d) where d is size of the stack in worst case scenario. – Uthman Jul 23 '19 at 00:13
  • 1
    It would be O(n) where n is the number of total items in the whole nested object structure. The depth is not relevant. It could just as easily be a single object with the same number of items. There just wouldn't be as much overhead as jumping into nested arrays. – James Wilkins Jul 23 '19 at 19:19
7

How about this?

inp = [1, {a: 2}, [3], [[4, 5], 6], 7]
out = inp;

while(out.some(Array.isArray))
  out = [].concat.apply([], out);

document.write(JSON.stringify(out));
georg
  • 211,518
  • 52
  • 313
  • 390
  • Clever for its brevity, but it's slower than other solutions, because you call `some` at every iteration, and repeatedly reallocate the array using `concat`, with increasing complexity, potentially concatenating the same scalars over and over. – Whatabrain Oct 09 '20 at 02:41
4

Works, but not recommended:

var flatten = function(input) {
    return eval("[" + JSON.stringify(input).
    replace(/\[/g,"").replace(/\]/g,"") + "]");
}
L3viathan
  • 26,748
  • 2
  • 58
  • 81
  • As nasty as that is, it might actually be able to handle larger arrays than the recursive or iterative solutions. A shootout would be awesome :-) – U Avalos Jul 19 '16 at 02:55
  • what if the array contains some string such as `"go to supermarket [...]"` – nonopolarity Dec 18 '19 at 04:46
  • @nopole Then it won't work, but it does for OPs case with numbers. See also "but not recommended". – L3viathan Dec 18 '19 at 08:08
  • Amazing. Very slow because of `JSON.stringify`, but deserves an upvote anyway. :) You can combine those two regexes to `replace(/[\[\]]/g,"")` – Whatabrain Oct 09 '20 at 02:44
3

Here's a solution that flattens in place.

function flatten(arr) {
  var i = 0;

  if (!Array.isArray(arr)) {
    /* return non-array inputs immediately to avoid errors */
    return arr;
  }

  while (i < arr.length) { 
    if (Array.isArray(arr[i])) {
      arr.splice(i, 1, ...arr[i]);
    } else {
      i++;
    }
  }
  return arr;
}

This solution iterates through the array, flattening each element one level of nesting at a time until it cannot be flattened any more.

Lope
  • 71
  • 1
  • 4
  • I've [benchmarked every solution](https://codepen.io/mindplay-dk/pen/EbeVGP) I could find on SO and jsperf, and this one was the fastest. It's also readable and should have good memory characteristics. Nice work! – mindplay.dk Nov 27 '17 at 15:15
  • This is a bad cross-platform solution since IE does not support the 'spread' operator (Edge does support it though, but IE has the larger market of the two at the moment). – James Wilkins Nov 29 '17 at 02:51
  • @mindplay.dk I checked out your benchmark page and georg has 22 million ops/sec, whereas Lope and Rick Edwards were both 17 million. Next fastest solution was 9 million. I'd love to see a super simple recursive solution added for comparison. Perhaps the original provided in the question. – Devin Rhode Jul 09 '19 at 03:24
  • This should be slower than the simple recursive solution, or using a stack and iteration. The other down-side is that repeated array reallocation using `splice` could result in a lot of garbage collection. – Whatabrain Oct 09 '20 at 02:51
3
function flatten(array){
  for(var i=0;i<array.length;i++)
    if(Array.isArray(array[i]))
      array.splice.apply(array,[i,1].concat(array[i--]));
  return array;
}

This in-place solution is faster than Lupe's, now that I've removed all of the inner curly brackets (I inlined the i-- in the concat parameter to do that).

1

A different iterative algorithm:

function flatten2(input) {
  var output = [];
  var todo = [input];
  var current;
  var head;

  while(todo.length) {
    var current = todo.shift();
    if(Array.isArray(current)) {
      current = current.slice();
      head = current.shift();
      if(current.length) {
        todo.unshift(current)
      }

      todo.unshift(head);
    } else {
      output.push(current);
    }
  }

  return output;
}
  • Put all elements on a stack.
  • While the stack is not empty, remove the first element.
    • If that element is a scalar, add it to the output.
    • If that element is an array, split it into head (first element) and tail (remaining elements) and add both to the stack.

As Tomalak's JSPerf shows, this is pretty slow.

JSBin

joews
  • 29,767
  • 10
  • 79
  • 91
0

A fairly concise, readable algorithm:

function flatten(input) {
  var output = [];
  var todo = [input];
  var current;

  while(todo.length) {
    var current = todo.shift();
    if(Array.isArray(current)) {
       todo.unshift.apply(todo, current)
    } else {
      output.push(current);
    }
  }

  return output;
}

This version performs better than my other answer, but is still significantly slower than James Wilkins' answer.

JSBin

Tomalak's JSPerf

Community
  • 1
  • 1
joews
  • 29,767
  • 10
  • 79
  • 91
0

Here are two approaches, recursive and iterative and their comparison to Array.flat. Maybe it'll help someone

const arrayToFlatten = [[1], [2, [3]], null, [[{}]], undefined];

// takes an array and flattens it recursively, default depth is 1 (just like Array.flat())
function flattenRecursive(arr, depth = 1) {
  let myArray = [];

  if (depth === 0){ // if you've reached the depth don't continue
    myArray = arr;
  } else if(!Array.isArray(arr)) { // add item to array if not an array
    myArray.push(arr);
  } else { // flatten each item in the array then concatenate
    arr.forEach(item => {
      const someNewArray = flattenRecursive(item, depth - 1);
      myArray = myArray.concat(someNewArray);
    });
  }
  
  return myArray;
}

// takes an array and flattens it using a loop, default depth is 1 (just like Array.flat())
function flattenIterative(arr, depth = 1) {
  let result = arr;

  // if an element is an array
  while(result.some(Array.isArray) && depth) {
    // flatten the array by one level by concating an empty array and result using apply
    result = [].concat.apply([], result);
    depth--; // track depth
  }

  return result;
}

console.log(arrayToFlatten.flat(2)); // ES^
console.log(flattenRecursive(arrayToFlatten, 2));
console.log(flattenIterative(arrayToFlatten, 2));
SoluableNonagon
  • 11,541
  • 11
  • 53
  • 98
0

Here's my solution to this:

function flattenList(A) {
  let result = []
  for (let i=0; i < A.length; i++) {
    if (typeof A[i] == "object"){
      let item = reduceArray(A[i])
      result.push(...item)
    }else {
      result.push(A[i])
    }
  }
  return result
}

function reduceArray(arr){
  while(arr.some(Array.isArray)) {
    let item = arr.find(Array.isArray)
    let index = arr.indexOf(item)
    arr[index] = item[0]
  }
  return arr
}
Willower
  • 1,099
  • 8
  • 22
0

Not sure if the "stack" approach was used properly in previous answers. I think it could be simpler, like this:

function flatten(arr) {

  const result = [];
  const stack = [arr];

  while (stack.length) {
    const curr = stack.pop();
    if (Array.isArray(curr)) {
      for (let i = curr.length - 1; i >= 0; i--) {
        stack.push(curr[i]);
      }
    } else {
      result.push(curr);
    }
  }

  return result;
}
Marisev
  • 451
  • 5
  • 6
0

Not sure why the other answers are so complicated, this can easily be achieved by looping through the array and flattening each entry until it's no longer an array.

const flatten = (arr) => {
  for (let i = 0; i < arr.length; i++) {
    while (Array.isArray(arr[i])) {
      arr.splice(i, 1, ...arr[i]);
    }
  }
  return arr;
}