As pointed out in the comments, since each element is indeed touched only once, the time complexity is intuitively O(N).

However, because each recursive call to flatten creates a new intermediate array, the run-time depends strongly on the structure of the input array.


A non-trivial1 example of such a case would be when the array is organized similarly to a full binary tree:

[[[a, b], [c, d]], [[e, f], [g, h]]], [[[i, j], [k, l]], [[m, n], [o, p]]]

               |
        ______ + ______
       |               |
    __ + __         __ + __
   |       |       |       |
 _ + _   _ + _   _ + _   _ + _
| | | | | | | | | | | | | | | | 
a b c d e f g h i j k l m n o p

The time complexity recurrence relation is:

T(n) = 2 * T(n / 2) + O(n)

Where 2 * T(n / 2) comes from recursive calls to flatten the sub-trees, and O(n) from pushing2 the results, which are two arrays of length n / 2.

The Master theorem states that in this case T(N) = O(N log N), not O(N) as expected.

1) non-trivial means that no element is wrapped unnecessarily, e.g. [[[a]]].

2) This implicitly assumes that k push operations are O(k) amortized, which is not guaranteed by the standard, but is still true for most implementations.


A "true" O(N) solution will directly append to the final output array instead of creating intermediate arrays:

function flatten_linear(items) {
  const flat = [];
  
  // do not call the whole function recursively
  // ... that's this mule function's job
  function inner(input) {
     if (Array.isArray(input))
        input.forEach(inner);
     else
        flat.push(input);
  }
  
  // call on the "root" array
  inner(items);  

  return flat;
}

The recurrence becomes T(n) = 2 * T(n / 2) + O(1) for the previous example, which is linear.

Again this assumes both 1) and 2).

Answer from meowgoesthedog on Stack Overflow
Top answer
1 of 1
11

As pointed out in the comments, since each element is indeed touched only once, the time complexity is intuitively O(N).

However, because each recursive call to flatten creates a new intermediate array, the run-time depends strongly on the structure of the input array.


A non-trivial1 example of such a case would be when the array is organized similarly to a full binary tree:

[[[a, b], [c, d]], [[e, f], [g, h]]], [[[i, j], [k, l]], [[m, n], [o, p]]]

               |
        ______ + ______
       |               |
    __ + __         __ + __
   |       |       |       |
 _ + _   _ + _   _ + _   _ + _
| | | | | | | | | | | | | | | | 
a b c d e f g h i j k l m n o p

The time complexity recurrence relation is:

T(n) = 2 * T(n / 2) + O(n)

Where 2 * T(n / 2) comes from recursive calls to flatten the sub-trees, and O(n) from pushing2 the results, which are two arrays of length n / 2.

The Master theorem states that in this case T(N) = O(N log N), not O(N) as expected.

1) non-trivial means that no element is wrapped unnecessarily, e.g. [[[a]]].

2) This implicitly assumes that k push operations are O(k) amortized, which is not guaranteed by the standard, but is still true for most implementations.


A "true" O(N) solution will directly append to the final output array instead of creating intermediate arrays:

function flatten_linear(items) {
  const flat = [];
  
  // do not call the whole function recursively
  // ... that's this mule function's job
  function inner(input) {
     if (Array.isArray(input))
        input.forEach(inner);
     else
        flat.push(input);
  }
  
  // call on the "root" array
  inner(items);  

  return flat;
}

The recurrence becomes T(n) = 2 * T(n / 2) + O(1) for the previous example, which is linear.

Again this assumes both 1) and 2).

Top answer
1 of 2
3

No, the code you've shown has neither exponential nor linear time complexity.

Before we can determine the complexity of any algorithm, we need to decide how to measure the size of the input. For the particular case of flatting an array, many options exist. We can count the number of arrays in the input, the number of array elements (sum of all array lengths), the average array length, the number of non-array elements in all arrays, the likelyhood that an array element is an array, the average number of elements that are arrays, etc.

I think what makes the most sense to gauge algorithms for this problem are the number of array elements in the whole input - let's call it e - and the average depth of these elements - let's call it d.

Now there are two standard approaches to this problem. The algorithm you've shown

const flattenDeep = (array) => {
  const flat = [];
  for (let element of array) {
    Array.isArray(element)
      ? flat.push(...flattenDeep(element))
      : flat.push(element);
  }
  return flat;
}

does have a time complexity of O(e * d). It is the naive approach, also demonstrated in the shorter code

const flattenDeep = x => Array.isArray(x) ? x.flatMap(flattenDeep) : [x];

or in the slightly longer loop

const flattenDeep = (array) => {
  const flat = [];
  for (const element of array) {
    if (Array.isArray(element)) {
      flat.push(...flattenDeep(element))
    } else {
      flat.push(element);
    }
  }
  return flat;
}

Both of them have the problem that they are more-or-less-explicit nested loops, where the inner one loops over the result of the recursive call. Notice that the spread syntax in the call flat.push(...flattenDeep(element)) amounts to basically

for (const val of flattenDeep(element)) flat.push(val);

This is pretty bad, consider what happens for the worst case input [[[…[[[1,2,3,…,n]]]…]]].

The second standard approach is to directly put non-array elements into the final result array - without creating, returning and iterating any temporary arrays:

function flattenDeep(array) {
  const flat = [];
  function recurse(val) {
    if (Array.isArray(val)) {
      for (const el of val) {
        recurse(el);
      }
    } else {
      flat.push(val);
    }
  }
  recurse(array);
  return flat;
}

This is a much better solution, it has a linear time complexity of O(e) - d doesn't factor in at all any more.

2 of 2
-2
  • the exploration of a regular structure can be done, however, for models that use addressing it is necessary to control the exploration, otherwise it may have infinite recursion.. i.e:

    var a = [ 1, 2, 3 ]; var t = [ ]; t.push(a); t.push(t);

    console.log(t);

infinit loop example

  • action: check if your array value already explored.
Discussions

Javascript Interview Question: Flatten an Array (in-depth)

if I were an interviewer, I think I would be satisfied by your answer, but as a fellow programmer I think that despite the current situation with tail call optimisation, at least attempting a tail recursive solution is worth it, because you can 1) be future proof for when the tail call situation improves and 2) use a trampoline to put a tail-call style recursive function into an iterative process in the meantime, which solves the issue.

Here is an example of a tail-recursive flatten function that uses a "trampoline" to put each recursive call into an iterative while loop. It's probably not the most efficient way of going about things, but due to the trampoline, there should be no stack growth issue. I also opted to not "return a continuation" for the continuation part of the trampoline. Returning a continuation function would increase the amount of inner function executions and definitions, so instead I just take a "snapshot" of the arguments and call the original function with those arguments.

const trampoline = fn => (...args) => {
  let step = fn(...args);
  while (!step.done) {
    step = fn(...step.args);
  }
  return step.result;
}

trampoline.done = result => ({ done: true, result });
trampoline.next = (...args) => ({ done: false, args });

const flatten = trampoline((list, accumulator = []) => {
  if (list.length === 0) {
    return trampoline.done(accumulator);
  }
  const [head, ...tail] = list;
  if (Array.isArray(head)) {
    return trampoline.next([...head, ...tail], accumulator);
  }
  return trampoline.next(tail, [...accumulator, head]);
});

console.log(flatten([1, 2, [3, [4, [5, [6, 7, [8, 9, 10]]]]]]));

More on reddit.com
🌐 r/javascript
13
6
October 25, 2018
What is the time complexity of array de-structuring?
With de-structuring, for example, this: const [a, b] = array; You are implicitly specifying the index, starting from zero and following the order of each new variable you define (a, b and so on) so this should be O(1) or constant time. Source. On the other hand if you do: const [a, b, ...rest] = array; The ...rest part (spread syntax) will have to loop through the rest of array to extract each element and put it in rest, so this will be O(n), with n being the number of remaining elements. Source. More on reddit.com
🌐 r/learnjavascript
29
22
December 15, 2022
A Smarter JavaScript Mapper: array.flatMap()
This is the type of overly clever code that I would reject if I saw it in a PR. I would even prefer reduce to this. More on reddit.com
🌐 r/javascript
17
15
January 5, 2022
🌐
GitHub
gist.github.com › nitely › 21735cf83c8867b9004e203e78aae76d
Flatten array in JavaScript · GitHub
Elements is the sum of integers and nested arrays. There may be a better way to express this in Big O notation, but idk how. [0]: that number is 3 between push, copy, and pop; it's not done for every element, is only done once. Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment · You can’t perform that action at this time.
🌐
Medium
medium.com › @inbasekaran18 › understanding-time-complexity-of-array-methods-in-javascript-cc7bb30b3e9d
Understanding Time Complexity of Array Methods in JavaScript | by Inbasekaran | Medium
May 29, 2024 - In this article, we’ll take a detailed look at the time complexities of common array methods and how they impact performance.
🌐
GeeksforGeeks
geeksforgeeks.org › javascript-array-flat-method
JavaScript Array flat() Method | GeeksforGeeks
July 12, 2024 - The code initializes a multilevel array, then applies the flat() method with Infinity parameter to recursively flatten all nested arrays into a single-level array.
🌐
Educative
educative.io › home › courses › master the javascript interview › flatten array
Flatten Array - Master the JavaScript Interview
It should return a new array that contains the items of each internal array, preserving order. ... flatten([ [ [ [1], 2], 3], [4], [], [[5]]]); // -> [1, 2, 3, 4, 5] flatten(['abc', ['def', ['ghi', ['jkl']]]]); // -> ['abc', 'def', 'ghi', 'jkl'] As in the last problem, we have to process every item we receive. There’s no way to get around that so the best time complexity we ...
🌐
DEV Community
dev.to › lukocastillo › time-complexity-big-0-for-javascript-array-methods-and-examples-mlg
Time complexity Big 0 for Javascript Array methods and examples. - DEV Community
June 3, 2020 - I think that it is very important to understand the time complexity for the common Array methods that we used to create our algorithms and in this way we can calculte the time complexity of the whole structure.
Find elsewhere
🌐
Reddit
reddit.com › r/javascript › javascript interview question: flatten an array (in-depth)
r/javascript on Reddit: Javascript Interview Question: Flatten an Array (in-depth)
October 25, 2018 -

Write a function that flattens a given input array. It can contain many deeply nested arrays. Example: given [[1],[[2]],[[[3]]]] the function should return [1,2,3].

I've been studying up on some common JS interview questions and this one really intrigued me. So, I spent a bit of time looking in depth at the problem and would love other's feedback and help to determine if I assessed it correctly. Also, if anyone else has been presented with this problem maybe this will help them clearly look at the trade-offs of iterative vs recursion in this specific situation.

Also, I think I commonly see the answer for time and space complexity to this question as O(n)... but I think that's wrong. See my analysis below.

Thanks in advance for the feedback and I hope this will help others when they need to answer a question like this.

https://gist.github.com/jcarroll2007/4ee72b3e99507c4f8ce3916fca147ab7

Top answer
1 of 5
2

if I were an interviewer, I think I would be satisfied by your answer, but as a fellow programmer I think that despite the current situation with tail call optimisation, at least attempting a tail recursive solution is worth it, because you can 1) be future proof for when the tail call situation improves and 2) use a trampoline to put a tail-call style recursive function into an iterative process in the meantime, which solves the issue.

Here is an example of a tail-recursive flatten function that uses a "trampoline" to put each recursive call into an iterative while loop. It's probably not the most efficient way of going about things, but due to the trampoline, there should be no stack growth issue. I also opted to not "return a continuation" for the continuation part of the trampoline. Returning a continuation function would increase the amount of inner function executions and definitions, so instead I just take a "snapshot" of the arguments and call the original function with those arguments.

const trampoline = fn => (...args) => {
  let step = fn(...args);
  while (!step.done) {
    step = fn(...step.args);
  }
  return step.result;
}

trampoline.done = result => ({ done: true, result });
trampoline.next = (...args) => ({ done: false, args });

const flatten = trampoline((list, accumulator = []) => {
  if (list.length === 0) {
    return trampoline.done(accumulator);
  }
  const [head, ...tail] = list;
  if (Array.isArray(head)) {
    return trampoline.next([...head, ...tail], accumulator);
  }
  return trampoline.next(tail, [...accumulator, head]);
});

console.log(flatten([1, 2, [3, [4, [5, [6, 7, [8, 9, 10]]]]]]));

2 of 5
1

const deepFlatten = arr => [].concat(...arr.map(v => (Array.isArray(v) ? deepFlatten(v) : v)));

example:

deepFlatten([1, [2], [[3], 4], 5]); // [1,2,3,4,5]

https://30secondsofcode.org/#deepflatten

🌐
Medium
codingwithmanny.medium.com › javascript-flatten-array-algorithm-311c2a714a04
JavaScript Flatten Array Algorithm | by Manny | Medium
April 11, 2020 - JavaScript Flatten Array Algorithm Breaking Down The Steamroller Algorithm Problem From FreeCodeCamp.org Goal We’re going to examine the problem of trying to flattening arrays in JavaScript or the …
🌐
GitHub
gist.github.com › thm-design › af870d56b608ae96acf052380a986e6a
Time & space complexity in javascript · GitHub
In general, the time complexity of rendering a list of components in React is O(n), where n is the number of components in the array. This is because React will render each component in the array exactly once.
🌐
Medium
medium.com › quick-code › considering-optimization-and-time-complexity-with-js-algorithms-4c8915086518
Considering Optimization and Time Complexity with JS Algorithms | by Jennifer Ingram | Quick Code | Medium
February 25, 2019 - When considering time complexity, best practice is to calculate the worst case scenario. Looking at our two arrays, that would be [4, 5, 7], because we know that this array does not include a double, so both loops (our while loop and .includes()) will run completely through the entire array because there is no true case that will break them out of the loops.
🌐
freeCodeCamp
freecodecamp.org › news › flatten-array-recursion
How to Flatten an Array in JavaScript Using Recursion
August 18, 2022 - Now, you know how to flatten an array using recursion. Recursion is an expensive approach when it comes to time and space complexity.
🌐
TutorialsPoint
tutorialspoint.com › flat-a-javascript-array-of-objects-into-an-object
Flat a JavaScript array of objects into an object
However, its space complexity is O(n) where n is the size of actual array. //code to flatten array of objects into an object //example array of objects const notes = [{ title: 'Hello world', id: 1 }, { title: 'Grab a coffee', id: 2 }, { title: 'Start coding', id: 3 }, { title: 'Have lunch', ...
🌐
GeeksforGeeks
geeksforgeeks.org › javascript-array-flatmap-method
JavaScript Array flatMap() Method | GeeksforGeeks
July 12, 2024 - Example 1: In this example the flatMap() method doubles and squares each number in the array, creating a new flattened array, making complex transformations manageable.
🌐
DEV Community
dev.to › ryan_dunton › flattening-an-array-a-performance-test-dka
Flattening an Array, a Performance Test - DEV Community
December 17, 2018 - This was actually fairly shocking to me. Does anyone have a better flattening method? ... You could speed it up a little by counting and pre-allocating the array. Because that's going to be the slowest part of the classical for loop - the array allocations attributed to the pushes.
🌐
Medium
medium.com › @kruthiv › javascript-series-arrays-part-4-8cf0332b7ef8
JavaScript Series: Arrays Part — 3 | by Kruthi Venkatesh | Medium
November 3, 2023 - Accessing elements in a JavaScript array is straightforward. Below, I will discuss how to access array elements, along with common operations associated with arrays and its time, space complexities.
🌐
Reddit
reddit.com › r/learnjavascript › what is the time complexity of array de-structuring?
r/learnjavascript on Reddit: What is the time complexity of array de-structuring?
December 15, 2022 - If it's just turning the array into some kind of keyvalue pair situation, by first guess with be o(n) ... Index access is O(1) as is pushing and popping. Shifting is O(n) ... Okay? Also why am I downvoted when I give the same answer as the top comment, just before, with less detail, lol? Okay guys ... The top comment gives the correct answer of O(1). You said O(n). ... There is nothing speculative about time-complexity.
🌐
Reddit
reddit.com › r/javascript › a smarter javascript mapper: array.flatmap()
r/javascript on Reddit: A Smarter JavaScript Mapper: array.flatMap()
January 5, 2022 - Agreed. I love flatMap, but it should be used for its intended purpose, take an array of arrays and flatten it. Overly clever code just becomes an annoying brain teaser you have to constantly resolve every time you see it.
🌐
Javascript360
javascript360.org › day 26: flatten deeply nested array
Day 26: Flatten Deeply Nested Array | JavaScript360.org
Therefore, the space complexity is O(n) as we need space for n recursive calls on the call stack. We can put all the children of the nested arrays in a queue for processing at a later iteration. We initialize a boolean variable nestedArrayElement to true, which indicates whether there are still nested arrays to be flattened.