引言
递归是指一个函数或过程直接或间接地调用自身的过程。基本思想是将一个复杂问题分解为一系列相似但规模更小的子问题,直到子问题变得足够简单,可以直接求解。然后,通过逐步合并这些子问题的解,最终得到原始问题的解。
一:斐波那契函数
递归函数通常包含以下两个重要部分:
- 基本情况(Base Case) :这是递归调用的终止条件。在
fibonacci
函数中,当n
等于0或1时,函数直接返回预定义的值,不再调用自身。这是最基本的情况,不需要进一步的递归就能得到结果。 - 递归情况(Recursive Case) :这是函数调用自身来解决更小规模的同一问题的部分。在
fibonacci
函数中,当n
大于1时,函数通过计算fibonacci(n - 1)
和fibonacci(n - 2)
来找到第n
个斐波那契数。这实际上是在解决两个规模更小的斐波那契数列问题,然后将它们的结果相加以获得最终答案。
function fibonacci(n) {
if (n <= 0) {
return 0;
}
if (n === 1) {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);//相似但规模更小的子问题
}
这种递归方法直观地反映了斐波那契数列的定义,但因为它包含大量的重复计算,导致它效率低下。例如,计算fibonacci(5)
时,fibonacci(3)
会被计算两次,fibonacci(2)
会被计算三次,等等。为了避免这种重复计算,可以使用记忆化或动态规划来优化斐波那契数列的计算,将已计算过的值存储起来供后续使用,从而显著提高效率。
下面是一个记忆化的例子:
function fibonacci(n, memo = {}) {
if (n <= 0) {
return 0;
}
if (n === 1) {
return 1;
}
if (memo[n] !== undefined) {
return memo[n];
}
memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo);
return memo[n];
}
二:数组降维
在现代JavaScript开发中,处理多维数组是家常便饭。数组扁平化,即将多维数组转换为单层数组,在大厂的面试题当中,这方面的内容也考察颇多。
2.1:flat()
实现数组降维
flat()
方法接受一个可选的深度参数,该参数指示要扁平化的数组层数。如果省略此参数,默认值为1,这意味着它只会扁平化数组的第一层级嵌套。- 当需要完全展平数组,无论嵌套有多深时,可以使用
Infinity
作为深度参数,尽管flat()
方法本身并不会真正展开无穷大的数组,但这个参数能够满足我们要尽可能多地展平数组的期望。
const arr = [1, 2, [3, 4, [5]]]//多维数组
console.log(arr.flat())// 输出[ 1, 2, 3, 4, [ 5 ] ]
console.log(arr.flat(1))// 输出[ 1, 2, 3, 4, [ 5 ] ]
console.log(arr.flat(2))// 输出输出[ 1, 2, 3, 4, 5 ]
console.log(arr.flat(3))// 输出输出[ 1, 2, 3, 4, 5 ]
console.log(arr.flat(Infinity))//Infinity 表示无穷大,flat() 方法不会展开无穷大的数组。
flat()
的优势
- 简洁性:
flat()
提供了一个直接的方法来展平数组,相比于传统的循环或递归方法,代码更为简洁明了。 - 可控性:通过指定深度参数,开发者可以精确控制扁平化的程度,避免不必要的数据处理。
2.2:普通递归实现数组降维
- 遍历与判断:遍历数组中的每个元素,判断当前元素是否为数组。如果不是数组,直接将元素添加到结果数组中;如果是数组,则进入下一步。
- 递归处理:对判断为数组的元素,再次调用扁平化函数,将子数组作为新的参数进行递归处理,然后将递归返回的扁平化结果合并到当前结果数组中。
function flatten(arr) {
let res = []
for(let i=0; i<arr.length;i++)
{
if(Array.isArray(arr[i])) res = [...res, ...flatten(arr[i])]//解构
else res.push(arr[i])
}
return res
}
普通递归的优势
- 灵活性与定制性:递归方法允许在过程中加入更多自定义逻辑,比如条件筛选、类型转换或特殊处理。
- 兼容性与控制:递归方法在各种环境中具有更好的兼容性,同时提供更细粒度的控制,特别是对于深度不确定的嵌套数组。
2.3:toString()
实现数组降维
- 利用
toString()
方法将数组转换成字符串 - 利用
.split(',')
将逗号作为分隔符将字符串分割 - 利用
.map()
遍历每一个元素 - 利用
Number()
将每一项转换为数字
const arr = [1, 2, [3, 4, [5]]];
console.log( arr.toString() // 将数组转换成字符串,输出为:"1,2,3,4,5"
.split(',') // 使用逗号作为分隔符将字符串分割,得到:"1", "2", "3", "4", "5"
.map((item) => { // 遍历每一个元素
return Number(item); // 将每一项转换为数字
})
);//输出[ 1, 2, 3, 4, 5 ]
toString的劣势
- 依赖于特定的数组结构:如果数组的结构稍微不同,比如
[1, 2, [[3], 4, 5]]
,toString()
方法的输出将无法被正确解析,因为"[3]"
和"[4, 5]"
会被视为单独的字符串元素,而不是数字。 - 非数字元素的处理:如果数组包含非数字元素,如字符串或对象,这种方法将失败,因为
Number()
将这些元素转换为NaN
。
2.4:reduce()
实现数组降维
- 遍历数组元素:
reduce
方法遍历数组中的每个元素,pre
是累积器,item
是当前元素。reduce
方法的初始值是一个空数组[]
,用于累积扁平化后的数组。 - 判断与处理:对于每个
item
,如果它是一个数组(由Array.isArray(item)
判断),则递归调用flatten(item)
来处理嵌套的子数组。如果不是数组,则直接将item
添加到累积器pre
中。 - 累积结果:
reduce
的回调函数执行,通过concat
方法将递归调用的结果或单个元素添加到累积器pre
中,reduce
方法返回累积的扁平化数组。
const arr = [1, 2, 'abc', [3, 4,[5]]]
function flatten(arr){
return arr.reduce((pre,item) =>{
// reduce的回调函数
return pre.concat(Array.isArray(item) ? flatten(item) : item)
},[])
}
console.log(flatten(arr));
reduce的优势
- 高效累积:
reduce
通过单一累积器高效处理所有数组元素,减少内存开销。 - 自然递归集成:递归自然融入
reduce
,优雅处理任意深度的嵌套数组,保持代码简洁。
2.5:解构实现数组降维
- 条件检查:使用
some
方法来检查数组中是否还存在子数组。遍历数组,如果发现任何一个元素是数组,则返回true
,表示数组中仍然有子数组需要处理。 - 扁平化操作:当检测到数组中存在子数组时,将数组解构为独立的元素,然后重新构造一个新数组,其中子数组的元素直接作为新数组的成员。
- 重复检查与操作:循环执行条件检查和扁平化操作,直到
some
方法不再返回true
,意味着数组中不再有任何子数组。
some
方法遍历数组中的每个元素,如果至少有一个元素使得测试函数返回true
,则整个some
方法返回true
。如果没有任何元素使得测试函数返回true
,则some
方法返回false
。
const arr = [1, 2, 'abc', [3, 4,[5]]]
function flatten(arr){
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr)// [1, 2, 'abc', 3, 4, [5]]
}
return arr
}
console.log(flatten(arr));
解构的优势
- 非递归实现:这种方法不使用递归,避免了递归可能导致的调用栈溢出问题,特别是在处理非常深的嵌套数组时。
- 动态检查:
some
方法动态检查数组中是否存在子数组,确保只有在必要时才进行扁平化操作,这使得代码更加高效和智能。
三:总结
以上探讨了递归的概念以及在复杂问题中的应用,重点介绍了几种实现数组扁平化的方法,包括递归、flat()
方法、toString()
方法、reduce()
方法和解构方法。
-
flat()
方法:这是用于数组扁平化的内置方法,简洁且易于使用,通过设置深度参数可以控制扁平化的程度。然而,对于兼容性要求较高的项目,flat()
可用性不高。 -
递归方法:将问题分解为处理数组中的每个元素,对于子数组,递归调用自身进行处理,直至所有子数组被完全展开。其关键在于定义基本情况和递归情况,确保递归的终止和子问题的解决。
-
toString()
方法:虽然可以用于特定的数组扁平化,但其可靠性受限于数组的结构,且无法处理非数字元素,因此不是通用的解决方案。 -
reduce()
方法:结合递归,reduce()
提供了高效且灵活的数组扁平化方案,能够处理任意深度的嵌套数组,同时保持代码的简洁性和高性能。 -
解构方法:使用
some
和while
循环,这种方法避免了递归带来的调用栈溢出风险,动态地检查和扁平化数组,确保了代码的高效和智能。