不可或缺的编程思想——递归

272 阅读8分钟

引言

递归是指一个函数或过程直接或间接地调用自身的过程。基本思想是将一个复杂问题分解为一系列相似但规模更小的子问题,直到子问题变得足够简单,可以直接求解。然后,通过逐步合并这些子问题的解,最终得到原始问题的解。

一:斐波那契函数

递归函数通常包含以下两个重要部分:

  1. 基本情况(Base Case) :这是递归调用的终止条件。在fibonacci函数中,当n等于0或1时,函数直接返回预定义的值,不再调用自身。这是最基本的情况,不需要进一步的递归就能得到结果。
  2. 递归情况(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()实现数组降维

  1. flat() 方法接受一个可选的深度参数,该参数指示要扁平化的数组层数。如果省略此参数,默认值为1,这意味着它只会扁平化数组的第一层级嵌套。
  2. 当需要完全展平数组,无论嵌套有多深时,可以使用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:普通递归实现数组降维

  1. 遍历与判断:遍历数组中的每个元素,判断当前元素是否为数组。如果不是数组,直接将元素添加到结果数组中;如果是数组,则进入下一步。
  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
}

普通递归的优势

  1. 灵活性与定制性:递归方法允许在过程中加入更多自定义逻辑,比如条件筛选、类型转换或特殊处理。
  2. 兼容性与控制:递归方法在各种环境中具有更好的兼容性,同时提供更细粒度的控制,特别是对于深度不确定的嵌套数组。

2.3:toString()实现数组降维

  1. 利用toString() 方法将数组转换成字符串
  2. 利用.split(',')将逗号作为分隔符将字符串分割
  3. 利用.map()遍历每一个元素
  4. 利用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. 依赖于特定的数组结构:如果数组的结构稍微不同,比如[1, 2, [[3], 4, 5]]toString()方法的输出将无法被正确解析,因为"[3]""[4, 5]"会被视为单独的字符串元素,而不是数字。
  2. 非数字元素的处理:如果数组包含非数字元素,如字符串或对象,这种方法将失败,因为Number()将这些元素转换为NaN

2.4:reduce()实现数组降维

  1. 遍历数组元素reduce方法遍历数组中的每个元素,pre是累积器,item是当前元素。reduce方法的初始值是一个空数组[],用于累积扁平化后的数组。
  2. 判断与处理:对于每个item,如果它是一个数组(由Array.isArray(item)判断),则递归调用flatten(item)来处理嵌套的子数组。如果不是数组,则直接将item添加到累积器pre中。
  3. 累积结果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的优势

  1. 高效累积reduce通过单一累积器高效处理所有数组元素,减少内存开销。
  2. 自然递归集成:递归自然融入reduce,优雅处理任意深度的嵌套数组,保持代码简洁。

2.5:解构实现数组降维

  1. 条件检查:使用some方法来检查数组中是否还存在子数组。遍历数组,如果发现任何一个元素是数组,则返回true,表示数组中仍然有子数组需要处理。
  2. 扁平化操作:当检测到数组中存在子数组时,将数组解构为独立的元素,然后重新构造一个新数组,其中子数组的元素直接作为新数组的成员。
  3. 重复检查与操作:循环执行条件检查和扁平化操作,直到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));

解构的优势

  1. 非递归实现:这种方法不使用递归,避免了递归可能导致的调用栈溢出问题,特别是在处理非常深的嵌套数组时。
  2. 动态检查some方法动态检查数组中是否存在子数组,确保只有在必要时才进行扁平化操作,这使得代码更加高效和智能。

三:总结

以上探讨了递归的概念以及在复杂问题中的应用,重点介绍了几种实现数组扁平化的方法,包括递归、flat()方法、toString()方法、reduce()方法和解构方法。

  1. flat()方法:这是用于数组扁平化的内置方法,简洁且易于使用,通过设置深度参数可以控制扁平化的程度。然而,对于兼容性要求较高的项目,flat()可用性不高。

  2. 递归方法:将问题分解为处理数组中的每个元素,对于子数组,递归调用自身进行处理,直至所有子数组被完全展开。其关键在于定义基本情况和递归情况,确保递归的终止和子问题的解决。

  3. toString()方法:虽然可以用于特定的数组扁平化,但其可靠性受限于数组的结构,且无法处理非数字元素,因此不是通用的解决方案。

  4. reduce()方法:结合递归,reduce()提供了高效且灵活的数组扁平化方案,能够处理任意深度的嵌套数组,同时保持代码的简洁性和高性能。

  5. 解构方法:使用somewhile循环,这种方法避免了递归带来的调用栈溢出风险,动态地检查和扁平化数组,确保了代码的高效和智能。