Skip to content

目录:

一、介绍

二、个人手写

1、个人未看源码思路

2、个人手写代码

2.1:递归

2.2:reduce

2.3:flat

2.4:堆栈

2.5:队列

三、源码分析

1、源码思路

四、总结

字数:大约900字

一、介绍

名称:arr-flatten

地址:https://github.com/jonschlinkert/arr-flatten.git

这个其实就是数组扁平化啦,面试中也经常遇到。

示例:

js
var flatten = require('arr-flatten');

flatten(['a', ['b', ['c']], 'd', ['e']]);
//=> ['a', 'b', 'c', 'd', 'e']

二、个人手写

这里按照简单的数组来实现,数组内用基本数据类型数据,所以ts也就不考虑具体类型限制啦。

1、个人未看源码思路

其实实现扁平化有多种方法哈,例如有递归、reduce、堆栈、队列、以及ES10引入的flat函数。我们把几种情况都写一下。

2、个人手写代码

2.1:递归

将一个多维数组分解成多个一维数组,对于每个一维数组都判断是否为数组类型,如果是数组类型,则继续递归直到该数组不再包含数组,然后将元素加入到结果数组中。下面是使用递归的实现方法:

ts
const flatten = (arr: any[]) => {
  let result: any[] = [];
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      result = result.concat(flatten(arr[i]));
    } else {
      result.push(arr[i]);
    }
  }
  return result;
};

测试

js
flatten(['a', ['b', ['c']], 'd', ['e']]);
//=> ['a', 'b', 'c', 'd', 'e']

性能:

数组递归扁平化的时间复杂度可以表示为 O(n^m),其中 n 是最后扁平化时的元素数量,m 是数组嵌套的最大深度。

在递归扁平化的过程中,需要遍历每个元素并判断其是否为数组,因此时间复杂度是与数组元素数量成线性关系的 O(n)。但由于存在嵌套的情况,递归的层数最多可以达到数组的最大深度 m,因此总的时间复杂度是 O(n^m)。

需要注意的是,由于递归过程中需要消耗堆栈空间,因此如果数组嵌套深度太大,可能会导致堆栈溢出的问题。因此,在处理大规模、深度较大的数组时,应该考虑非递归的扁平化方式。

2.2:reduce

reduce函数调用一个提供的回调函数并将每个元素依次归约为一个值。在这里,我们使用reduce函数将多维数组转换为一维数组。其实也是一个逻辑

js
const flatten = (arr: any[]): any[] => {
  return arr.reduce(
    (accumulator, currentValue) => accumulator.concat(
      Array.isArray(currentValue)
        ? flattenArray(currentValue)
        : currentValue
    ),
    []
  )
}

测试

js
flatten(['a', ['b', ['c']], 'd', ['e']]);
//=> ['a', 'b', 'c', 'd', 'e']

性能:

数组使用 reduce 方法进行扁平化操作,时间复杂度仍然是O(n)。

reduce 方法会对数组中的每一个元素执行一个回调函数,从当前元素开始进行计算,最终返回一个减少的值。在使用 reduce 进行扁平化时,回调函数的操作需要考虑到当前元素是否为数组,如果是数组,则进行递归处理。

由于必须遍历每个元素进行计算,因此时间复杂度是与数组元素数量成线性关系的 O(n)。但是,与递归扁平化不同,reduce 方法没有堆栈限制,因此不会因为嵌套深度太大而导致堆栈溢出的问题。

综上所述,使用 reduce 进行扁平化的性能与使用递归方法类似,但在处理大规模、深度较大的数组时,考虑使用 reduce 可能更可靠一些。

2.3:flat

ES2019推出了一个新函数flat,可以将多维数组变成一维数组。flat函数接收一个参数,用来指定要扁平化的深度,默认为1,如果设置为Infinity,则会将数组扁平化至最深层级。

js
const flatten = (arr: any[]) => {
  return arr.flat(Infinity);
}

测试

js
flatten(['a', ['b', ['c']], 'd', ['e']]);
//=> ['a', 'b', 'c', 'd', 'e']

性能

flat() 方法的时间复杂度是 O(n),与 reduce() 方法类似。

具体来说,flat() 方法是一种单层级扁平化操作,可以通过参数指定需要扁平化的层数。在一个数组中,每个元素仅被遍历一次,因此它的时间复杂度是线性的 O(n)。

与递归扁平化方法相比,flat() 方法具有更好的性能表现,而且相较于 reduce() 方法,flat() 方法需要更少的代码量和更高的可读性。同时,flat() 方法还可以通过参数级别控制扁平化的深度,更加灵活。

综上所述,flat>reduce>递归。

2.4:堆栈

借助堆栈(Stack)这种数据结构,我们可以将多维数组转化成一维数组。当遇到数组元素时,将其入栈;当遇到不是数组元素时,将其出栈并加入到结果数组中。

js
const flattenArray = (array: any[]): any[] => {
  const stack = [...array];
  const result = [];

  while (stack.length) {
    const nextItem = stack.pop();

    if (Array.isArray(nextItem)) {
      stack.push(...nextItem);
    } else {
      result.unshift(nextItem);
    }
  }

  return result;
}

性能:

使用堆栈算法将嵌套数组扁平化的时间复杂度为o(n),其中n是所有嵌套数组元素的数量。在使用堆栈算法时,我们遍历原始数组并将每个元素添加到结果数组中。如果当前元素是一个数组,则我们将其压入堆栈。在下一次迭代中,我们从堆栈中取出元素并将其添加到结果数组中,直到堆栈为空。

2.5:队列

借助队列(Queue)这种数据结构,我们也可以将多维数组转化成一维数组。当遇到数组元素时,将其加入到队列末尾;当遇到不是数组元素时,将其加入到结果数组中。使用队列时,需要注意遍历顺序,因为遍历的顺序会影响最终结果。

js
const flatten = (arr: any[]): any[] => {
  const result = [];
  const queue = [...arr];

  while (queue.length) {
    const next = queue.shift();
    if (Array.isArray(next)) {
      queue.push(...next);
    } else {
      result.push(next);
    }
  }

  return result;
}

性能:

因为我们只需遍历每个元素一次,所以时间复杂度为o(n)。然而,将大量元素压入堆栈可能会占用大量内存空间,这也需要考虑到。

使用队列算法对 js 数组进行扁平化的时间复杂度为 o(n),其中 n 是数组中元素的总数量。这是因为在使用队列算法进行扁平化时,我们需要遍历整个数组并将每个元素加入到队列中。然后,我们需要从队列中依次取出每个元素,并将非数组类型的元素添加到结果数组中,将数组类型的元素展开(即将其所有子元素加入到队列尾部)。

由于我们将每个元素都恰好处理一次,并且队列中的元素数量最多为n,因此时间复杂度为o(n)。需要注意的是,某些数组元素可能是嵌套很深的数组。在这种情况下,队列的长度可能会增长得比较快,导致算法的性能变差。

三、源码分析

那我们就要看看人家写的源码。

js
module.exports = function (arr) {
  return flat(arr, []);
};

function flat(arr, res) {
  var i = 0, cur;
  var len = arr.length;
  for (; i < len; i++) {
    cur = arr[i];
    Array.isArray(cur) ? flat(cur, res) : res.push(cur);
  }
  return res;
}

1、源码思路

发现它用的方法就是递归啦。学习了上面几种扁平化思路,再来看它这个源码就是很简单的啦。再也不用担心我面试遇到了。

四、总结

1、凡是用到递归实现的,都可以使用队列或者堆栈来替换,可以得到好的效果

2、...扩展符复制

对于数组来说,它使用了可变参数(rest)的语法,可以将一个数组展开成多个独立的值。使用 ... 进行数组的扩展操作时,它会基于源数组创建一个新的数组实例,新数组的元素与源数组相同,但是它们是不同的引用。也就是说,当你改变源数组中的元素时,新数组中的元素不会受到影响,反之亦然。

而对于对象来说,使用 ... 进行对象的扩展操作时,它会将源对象的所有可枚举属性复制到新的对象中。这意味着,新对象中的属性与源对象中的属性具有相同的值和名称,但是它们是不同的引用。因此,在其中一个对象中修改属性时,另一个对象中的属性不会受到影响。