🌅面试重点:数组扁平化有哪些方式(超详全解)
前言
在当今日益激烈的职场竞争中,掌握数组扁平化的知识对于面试者而言具有至关重要的意义。数据扁平化作为数据处理和算法设计中的一个重要环节,不仅展示了面试者对数据结构及其操作的深入理解,同时也反映了面试者解决复杂问题的能力。在面试中,能够熟练地使用数组扁平化技巧,将有助于提高面试者的代码质量和编程效率,从而给面试官留下深刻的印象,增加获得心仪职位的机会。但是,单单掌握一种方式去解答面试官的题目往往是难以让面试官满意的,今天我们就来一个解决数组扁平化方法的大盘点,举一反三拿下面试官!
递归
在正片开始之前我们必须了解掌握一个知识点----什么是递归?
什么是递归
递归(Recursion)是一种在函数定义中使用函数自身的方式。在编程中,递归是一种重要的算法策略,它通过将问题分解为更小、更简单的子问题来解决复杂问题。当函数直接或间接地调用自身时,就发生了递归。递归函数通常包含两部分:基准情况(Base Case)和递归情况(Recursive Case)。
基准情况
基准情况是递归停止的条件,即函数不再调用自身的情况。当达到基准情况时,函数会返回一个值,这个值可能是问题的解,也可能是用于计算更复杂解的部分结果。
递归情况
递归情况是函数调用自身的情况。在递归情况中,函数会将问题分解为更小的子问题,并对每个子问题调用自身。每个递归调用都会返回一个值,这个值将用于计算原问题的解。
递归在计算机科学中有很多应用,包括排序算法(如快速排序和归并排序)、搜索算法(如二分搜索)、数据结构(如树和图)的遍历、动态规划等。递归提供了一种简洁、优雅的方式来解决复杂问题,但也需要谨慎使用,以避免栈溢出等问题。
运用递归
了解递归是为了运用递归掌握递归,递归主要被运用在两方面上
1.阶乘计算
阶乘是一个数的所有正整数的乘积,如 5! = 5 × 4 × 3 × 2 × 1。使用递归可以很易地计算阶乘。
function mul(n) {
if (n === 0 || n === 1) {
return 1; // 基准情况
} else {
return n * mul(n - 1); // 递归情况
}
}
console.log(mul(5)); // 输出 120
在上面代码中if (n === 0 || n === 1) {
是一个条件语句,检查 n
是否等于 0 或 1; 如果 n
等于 0 或 1,则函数返回 1。这是递归的基准情况,用于停止递归并返回一个值。在数学上,0 的阶乘和 1 的阶乘都定义为 1。 return n * mul(n - 1)
是递归的核心部分。函数返回 n
乘以 n-1
的阶乘。为了计算 n-1
的阶乘,函数会再次调用自身(递归调用),这就是递归情况。
2.斐波那契数列
斐波那契数列是一个常见的数列,其中每个数字是前两个数字的和,例如 0, 1, 1, 2, 3, 5, 8, 13, ...。
function fb(n) {
if (n <= 1) {
return n; // 基准情况
} else {
return fb(n - 1) + fb(n - 2); // 递归情况
}
}
console.log(fb(10)); // 输出 55
注意:尽管递归对于理解斐波那契数列很有帮助,但对于大数值,递归实现可能会导致性能问题,因为它会进行大量的重复计算。在实际应用中,通常会使用动态规划或迭代来优化性能。
分析思路
知道了递归相关的知识后我们还不够,我们还得知道怎么判断数组,拼接数组
判断一个玩意是否是数组有几种方法?
1)判断一个玩意是不是数组我们可以使用Array.isArray()
方法
这是ES5中引入的一个方法,专门用于判断一个对象是否是数组。
let obj = [1, 2, 3];
console.log(Array.isArray(obj)); // 输出: true
2)我们还可以使用 instanceof
操作符来判断
let obj = [1, 2, 3];
console.log(obj instanceof Array); // 输出: true
3)我们还可以使用Object.prototype.toString.call()
方法
这个方法会返回一个表示该对象的字符串。对于数组,返回的字符串将是 "[object Array]"
。
let obj = [1, 2, 3];
console.log(Object.prototype.toString.call(obj) === "[object Array]");
// 输出: true
4)使用 Array.prototype.isPrototypeOf()
方法
如果 Array.prototype
是对象的原型链中的一部分,那么这个方法将返回 true
。
let obj = [1, 2, 3];
console.log(Array.prototype.isPrototypeOf(obj));
// 输出: true
合并数组有哪些方法?
1)使用 concat()
方法
concat()
方法用于接收数组中的零散值,连接两个或多个数组,并返回一个新数组,原数组不会被改变。
let arr = [1, 2]
let arr2 = [3, 4]
let arr3 = [5, 6]
let allArr = arr.concat(arr2, arr3)
//let allArr = arr.concat(arr2).concat(arr3)
// 输出: [1, 2, 3, 4, 5, 6]
如上所示,我们创建了一个新数组allArr,他包含了arr,arr2,arr3中的所有元素。
2)使用扩展运算符(Spread Operator)...
即解构
扩展运算符可以将一个数组的元素展开到另一个数组中,常用于函数参数、数组字面量等。
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let allArr = [...arr1, ...arr2];
console.log(allArr); // 输出: [1, 2, 3, 4, 5, 6]
通过使用扩展运算符来拼接数组是和concat()
同样简便的方法,原数组本身并不会改变
3)使用 push.apply()
方法(适用于旧版浏览器)
虽然 push()
方法通常用于在数组的末尾添加一个或多个元素,但结合 apply()
方法,可以将另一个数组的所有元素添加到当前数组的末尾。注意,这会改变原数组。
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
Array.prototype.push.apply(arr1, arr2);
console.log(arr1); // 输出: [1, 2, 3, 4, 5, 6]
在大多数情况下,concat()
和扩展运算符 ...
是合并数组的最常用和最简单的方法。如果你需要改变原数组,可以使用 push()
方法结合扩展运算符。对于更复杂的合并逻辑,可能需要使用 reduce()
或其他数组方法。
数组的遍历方法-reduce
reduce
数组身上自带的遍历方法,他接收两个参数,第一个是回调函数,第二个是这个函数的初始值,reduce里面的回调接收四个参数,其中自带的参数有累计效果,所以通常被用来累加。
let arr = [1, 2, 3, 4, 5, 6, 7]
let sum = arr.reduce(function (pre, item, index, arr) {
return pre + item
}, 0)
console.log(sum);
在这个例子中:
pre
是累加器的当前值,它保存了到目前为止所有项的和。在第一次迭代中,它是初始值0
,第一次执行结束,他被赋予成了1;在第二次迭代结束为1+2=3,第三次迭代的初始值为3......item
是当前正在处理的数组元素。在第一次的迭代中,他的初始值为1。index
是当前正在处理的元素的索引,但在这个例子中我们没有使用它。arr
是调用reduce
方法的数组(尽管在这个回调函数内部使用arr
作为变量名可能会导致混淆,因为我们已经有了外部定义的arr
变量,但在这里它是有效的,因为reduce
会将数组作为参数传递给回调函数)。
每次迭代,回调函数都会返回一个新的累加器值(即 pre + item
),这个值将成为下一次迭代中的 pre
。当所有元素都被处理过后,reduce
方法将返回最终的累加器值,也就是数组所有元素的和。在这个例子中,结果是 28
。
正面回答面试题
今天面试官就举出一个例子来考察你,问:
如何使数组[1, 2, [3, 4, [5]]]
扁平化?
答: 使多维数组扁平化,就是将嵌套数组转换为一维数组。在JavaScript中有多种方法可以实现,我将一一为您列举:
1. 使用flat
方法(ES2019引入)
flat
方法是 ES2019 (ES10) 中引入的一个数组方法,用于将嵌套的数组“扁平化”为一个新的数组。这个方法接收一个可选的参数 depth
,它指定了扁平化的深度。如果 depth
被省略或者大于嵌套数组的最大深度,那么它会一直扁平化到只剩一维数组。如果 depth
为 0,那么它将返回原数组。使用flat方法使我们能够轻松的扁平化数组。
let arr = [1, 2, [3, 4, [5]]];
let flatten = arr.flat(Infinity);
// Infinity 作为深度参数将展开任意深度的嵌套数组
console.log(flatten); // 输出 [1, 2, 3, 4, 5]
你看,对于题中的数组来说,我们将一个嵌套的数组 arr
转换为一个新的数组,这个新数组无论是 flatten还是什么。我们为arr数组加上flat方法,传入参数Infinity(无穷大);此时arr.flat(Infinity)
会创建一个新的数组,其中包含了 arr
中所有子数组的元素,而不仅仅是子数组本身。这个过程会递归地应用于 arr
中的所有嵌套数组,直到没有更多的嵌套数组为止,最终扁平化arr。
2. 使用递归
我们除了用flat外还可以选择利用递归手搓一个函数来实现扁平化。
我们需要一个空集来收集扁平化后的结果,即判断其中的数组将其中数组变为元素后储存到新集合中。
我们首先定义一个函数flatten,然后定义一个空数组res,用于存储扁平化后的结果
然后遍历这个数组arr,其中包含对arr中是否含有数组的判断。如果arr[i]中的元素存在数组,就会递归调用 flatten
函数,最后将扁平化的结果连接。
const arr = [1, 2, [3, 4, [5]]]
function flatten(arr) {
let res = []
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
res = res.concat(flatten(arr[i]))
} else {
res.push(arr[i])
}
}
return res
}
const newArr = flatten(arr)
console.log(newArr);
其中重点是 res = res.concat(flatten(arr[i]));
他主要做了四件事:
1)检查当前元素是否为数组:通过 Array.isArray(arr[i])
判断当前遍历到的元素 arr[i]
是否是一个数组。
2)递归调用 flatten
函数:如果 arr[i]
是一个数组(即嵌套数组),那么递归地调用 flatten
函数来处理这个嵌套数组。递归调用会返回这个嵌套数组扁平化后的结果。
3)连接数组:res.concat(flatten(arr[i]))
这部分代码使用了数组的 concat
方法。concat
方法用于连接两个或多个数组,并返回连接后的新数组。这里,它将 res
(到目前为止已经扁平化的元素)和 flatten(arr[i])
(嵌套数组扁平化后的结果)连接起来,形成一个新的数组。
4)更新 res
:最后,通过赋值操作 res = ...
,将连接后的新数组赋值给 res
。这样,res
就包含了到目前为止所有扁平化的元素,包括刚刚处理过的嵌套数组的内容。
这个递归过程会一直进行,直到没有更多的嵌套数组需要处理为止。最终,res
将包含原始数组 arr
中所有元素(无论它们是否嵌套在子数组中)的扁平化结果。
3. 使用reduce
和concat
在了解了reduce和concat的用法之后,我们当然可以利用他们的性质来优化数组扁平化;
const arr = [1, 2, [3, 4, [5]]]
function flatten(arr) {
return arr.reduce((pre, item) => {
return pre.concat(Array.isArray(item) ? flatten(item) : item)
}, [])
}
console.log(flatten(arr));
在这种方法中,核心点有两个:
1)使用 reduce 方法
return arr.reduce((pre, item) => {
reduce
方法用于遍历数组 arr
中的每个元素。pre
是累加器(accumulator),用于存储到目前为止处理过的元素的累积结果;item
是当前正在处理的元素。
2)递归调用 flatten:
return pre.concat(Array.isArray(item) ? flatten(item) : item);
对于 item
,这段代码首先检查它是否是一个数组(Array.isArray(item)
)。
- 如果
item
是一个数组,那么它递归地调用flatten(item)
来处理嵌套的数组,并返回一个新的一维数组。 - 如果
item
不是一个数组(即它是一个基本数据类型或其他非数组对象),那么它直接添加到累加器pre
中。
接下来,concat
方法用于将当前处理过的元素(可能是一个数组或单个值)与累加器 pre
合并,生成一个新的数组。最终这个数组就是我们成功扁平化的数组。
4. 通过解构(...
)和数组的some方法
使用递归和 Array.prototype.some
方法(意为数组隐式原型上的some方法)来检查数组中是否还有嵌套数组,然后使用 Array.prototype.map
和 ...
来扁平化数组。如果数组中的某个元素还是数组,那么就递归调用 flatten
函数来处理那个子数组。
const arr = [1, 2, [3, 4, [5]]];
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr.map(item => Array.isArray(item) ? flatten(item) : item));
}
return arr;
}
console.log(flatten(arr)); // 输出 [1, 2, 3, 4, 5]
这套方法同样有两个核心点:
1)使用while 循环
while (arr.some(item => Array.isArray(item))) {
这个循环会持续进行,直到数组 arr
中没有任何嵌套数组为止。some
方法会检查数组 arr
中的每一个元素。如果它找到一个元素是数组,那么 some
方法就会立即返回 true
,并结束遍历。这意味着只要 arr
中还有嵌套数组,循环就会继续。
2.map``...``concat
遍历解构和拼接
arr = [].concat(...arr.map(item => Array.isArray(item) ? flatten(item) : item));
这是实现数组扁平化的关键部分。
arr.map(...)
: 使用map
方法遍历数组arr
中的每一个元素。Array.isArray(item) ? flatten(item) : item
: 这是一个三元表达式。如果当前元素item
是一个数组(Array.isArray(item)
返回true
),那么就递归地调用flatten(item)
来扁平化这个子数组。如果item
不是一个数组,那么就原样返回它。...
: 展开运算符。它将map
方法返回的数组中的每个元素(这些元素本身可能是数组或单个值)作为独立的参数传递给concat
方法。[].concat(...)
:concat
方法用于连接两个或多个数组。但在这里,我们只使用了一个空数组[]
作为第一个参数,目的是创建一个新的数组来存储扁平化后的结果。
总结一下,这两行代码的作用是:遍历 arr
中的每一个元素,如果元素是数组,就递归地扁平化它;如果元素不是数组,就原样保留。然后,使用 concat
方法将扁平化后的结果连接成一个新的数组,并将这个新数组赋值给 arr
。经过这两行代码,无论初始数组 arr
的嵌套有多深,这个函数都能将其扁平化为一维数组。
结语
这些方法各有优缺点,可以根据具体的应用场景选择最适合的方法。例如,flat
方法非常简洁,但在某些旧版本的JavaScript中可能不可用。递归方法简单易懂,但在深度很大的数组上可能会导致调用栈溢出。使用栈的方法可以避免这个问题,但代码相对复杂一些。
经过与面试官的深入交流,特别是在数组扁平化这一问题上展现出的清晰思路与举一反三的能力,我相信我们肯定已经给面试官留下了深刻的印象。面试官可能会想:“这位应聘者不仅准确回答了问题,还展示了出色的逻辑思维和问题解决能力,真是个不可多得的人才!”
在迈向顶尖大厂的征途上,我们仍需不断学习、积累知识。然而,我们无需畏惧挑战,因为无论风雨如何,我都会与你并肩前行。让我们共同坚信,只要我们坚持不懈,始终保持着对技术的热情和对梦想的追求,终有一天我们会成功进入心仪的大厂。加油,让我们携手共进,为实现目标而努力奋斗!
转载自:https://juejin.cn/post/7371318721905180698