【问题标题】:Iterative solution for flattening n-th nested arrays in Javascript在Javascript中展平第n个嵌套数组的迭代解决方案
【发布时间】:2015-07-11 12:43:09
【问题描述】:

谁能告诉我以下问题的迭代解决方案?我以递归方式解决了它,但在迭代解决方案中遇到了困难。 (Facebook 技术面试题)

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

解决方案必须适用于第 n 个嵌套数组元素(即,如果有人修改了上面示例中的数组值/位置,它必须仍然有效)

递归解:

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

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

    return result;
}

【问题讨论】:

  • 出于好奇,您的递归解决方案是什么?
  • 这是作业吗?您为迭代解决方案尝试了什么?如果您已经有了递归解决方案,为什么还要追求迭代解决方案?
  • 请将递归解决方案添加到您可以正确格式化的问题中。多行代码在 cmets 中不可读。
  • 只是出于好奇,输入[1, {a: [2, [3, 4]]}] 会发生什么?输出是否应该与输入或[1, {a: [2, 3, 4]}](或其他)相同?换句话说,展平过程应该到达对象内部还是只是展平本身就是数组的元素?

标签: javascript arrays function


【解决方案1】:

不确定在以前的答案中是否正确使用了“堆栈”方法。我认为它可以更简单,像这样:

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;
}

【讨论】:

    【解决方案2】:

    这是我的解决方案:

    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
    }
    

    【讨论】:

      【解决方案3】:

      这里有两种方法,递归和迭代,以及它们与 Array.flat 的比较。 也许它会帮助某人

      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));

      【讨论】:

      【解决方案4】:
      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;
      }
      

      这个就地解决方案比 Lupe 的更快,因为我已经删除了所有内部大括号(我在 concat 参数中内联了 i-- 来做到这一点)。

      【讨论】:

      • 几乎和上面 Lope 的速度一样快——如果你没有 ES6 就好了。
      • 对 Lope 的回答非常好,以获得更好的浏览器支持。这似乎是不关心 GC 的最快速度,并且包含 IE 是一项要求——我想这对大多数人来说都是如此。 ;) .
      • 在这里调用.apply 有什么好处?为什么不直接做array.splice(i, 1, ...array[i--])
      • ...数组在 ES5 中不可用。
      【解决方案5】:

      这是一种方法:

      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);
      

      解释:如果迭代嵌套结构,你只需要在移动到嵌套数组之前通过保存当前数组和位置来记住你之前的位置(这通常通过递归解决方案的堆栈)。

      注意:如果您重复使用数组placeHolderlastIndex,您将不需要每次都重新创建它们。也许是这样的:

      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;
          }
      }();
      

      这再次更快(对于平面迭代),并且多次调用它的垃圾收集器问题更少。速度非常接近 Chrome 中递归函数调用的速度,比 FireFox 和 IE 中的递归快很多倍。

      我在这里重新创建了 Tomalak 的测试,因为旧的 jsPerf 无法编辑:https://jsperf.com/iterative-array-flatten-2

      【讨论】:

      • 我会删除我的,反正它们是等价的。
      • 这可能更符合我认为面试官正在寻找的内容。
      • @JamesWilkins 我尝试了更多平台。您的代码始终是最快的,做得很好。 :)
      • 谢谢。我在这里也做了类似的事情,以表明有时低级代码虽然复杂,但在特殊情况下可以更快:stackoverflow.com/a/25436773/1236397
      • 这将是 O(n),其中 n 是整个嵌套对象结构中的总项目数。深度无关。它可以很容易地成为具有相同数量项目的单个对象。不会像跳入嵌套数组那样开销太大。
      【解决方案6】:

      这是一个原地变平的解决方案。

      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;
      }
      

      此解决方案遍历数组,一次将每个元素展平一层嵌套,直到无法再展平为止。

      【讨论】:

      • 太棒了。比其他解决方案更简单,可能更快,内存效率更高。
      • 我有 benchmarked every solution 我可以在 SO 和 jsperf 上找到,这个是最快的。它也是可读的并且应该具有良好的记忆特性。干得好!
      • 这是一个糟糕的跨平台解决方案,因为 IE 不支持“传播”运算符(虽然 Edge 确实支持它,但 IE 目前拥有两者中更大的市场)。
      • @mindplay.dk 我查看了你的基准页面,georg 有 2200 万次操作/秒,而 Lope 和 Rick Edwards 都是 1700 万次。下一个最快的解决方案是 900 万。我很乐意看到添加了一个超级简单的递归解决方案来进行比较。也许是问题中提供的原件。
      • 这应该比简单的递归解决方案慢,或者使用堆栈和迭代。另一个缺点是使用splice 重复数组重新分配可能会导致大量垃圾回收。
      【解决方案7】:

      相当简洁易读的算法:

      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;
      }
      

      这个版本的性能比我的other answer 好,但仍然比James Wilkins' answer 慢很多。

      JSBin

      Tomalak's JSPerf

      【讨论】:

      • @Tomalak 你让我想到了性能 :-) 这更好,但与 James Wilkins 的回答不符。我链接了您的 JSPerf 的新版本。
      【解决方案8】:

      不同的迭代算法:

      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;
      }
      
      • 将所有元素放在一个堆栈上。
      • 当堆栈不为空时,删除第一个元素。
        • 如果该元素是标量,则将其添加到输出中。
        • 如果该元素是一个数组,则将其拆分为头部(第一个元素)和尾部(其余元素)并将两者都添加到堆栈中。

      正如Tomalak's JSPerf 所示,这很慢。

      JSBin

      【讨论】:

      • 仅供参考,到目前为止,我已经为答案制作了一个 jsPerf:jsperf.com/iterative-array-flatten
      • 谢谢。我追求的是性能上的清晰——那些slice 调用很慢。
      【解决方案9】:

      这个怎么样?

      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));

      【讨论】:

      • 聪明的简洁,但它比其他解决方案慢,因为你在每次迭代时调用some,并使用concat重复重新分配数组,随着复杂性的增加,可能会连接相同的标量及以上。
      【解决方案10】:

      有效,但不推荐:

      var flatten = function(input) {
          return eval("[" + JSON.stringify(input).
          replace(/\[/g,"").replace(/\]/g,"") + "]");
      }
      

      【讨论】:

      • 不太像我预期的解决方案。 :)
      • 尽管如此讨厌,它实际上可能能够处理比递归或迭代解决方案更大的数组。枪战会很棒:-)
      • 跳出框框思考的好方法。 ;)
      • 如果数组包含一些字符串,例如"go to supermarket [...]"
      • @nopole 然后它不起作用,但它适用于带有数字的 OPs 案例。另见“但不推荐”。
      猜你喜欢
      • 2013-07-22
      • 2019-03-14
      • 1970-01-01
      • 2022-01-01
      • 2021-05-12
      • 1970-01-01
      • 2020-01-26
      • 1970-01-01
      • 2016-03-19
      相关资源
      最近更新 更多