前端旧约

今天做别人不愿意做的事, 明天做别人不能做的事

小白眼中的递归

作为一个小白,涉及到递归首先想到的就是必须要有递归终止条件否则就会构成死循环。

对于递归还主要有两个地方不是很清楚,第一就是什么情况下可以用递归,第二个就是为什么递归中总是会用到 return。

先从一个最简单的计算阶乘的例子讲起

function factorial (n) {
  if (n === 1) {
    return 1
  }
  return n * factorial(n - 1)
}

console.log(factorial(3))

执行顺序:

factorial(3) ==> 3 * factorial(2) ==> 3 * 2 * factorial(1) ==> 3 * 2 * 1

问一个问题,在执行 factorial(3) 时如果没有 return 3 * factorial(2) 中的 return,那么 factorial(3) 就没有返回值,那么无论内部如何递归,它的值也是 undefined。

接下来再讲一下为什么可以使用递归。只要能将一个大问题分解为一个小问题,并且小问题的做法和大问题的做法是相同的,只是规模不一样,还有一点就是要使用上次的计算结果(关于这点有两种情况,一种是直接利用像上面计算阶乘的例子,还有一种是不直接利用,下面的一个例子就是这种情况)那么就可以考虑使用递归。

接下来再举一个例子,在 LeetCode 中的第 17 题 电话号码的字母组合。只谈我们关心的地方,电话号码上的 2-9 数字每个数字都会对应 3 或 4 个英文字母,用户在输入2 -9 的几个数字组成的字符串之后,我们需要将这个几个数字对应的字母进行组合,每个数字上出一个字母。

示例

输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].

首先创建 map 数组建立数字和字母之间的映射,然后解析传入的数字字符串将对应的字母存入到数组 code 中,例如 '23'=>['abc', 'def']。由于传入的数字个数是随机的,所以在将 code 数组中的前两项的字母进行组合之后存入到 tmp 数组中,利用 code.splice(0, 2, tmp) 将 code 中的前两项替换为 tmp,然后判断 code 数组的长度是否为 1,如果不是则继续调用 comb 函数(递归)将之前组合好的字母和后面的字母进行组合,如果是 1 则说明组合完成。

const f = (str) => {
  // 建立数字和字母之间的映射,例如map[2]正好是号码2对应的字母
  let map = ['', 1, 'abc', 'edf', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz']
  // 将传入的数字字符串分隔成单个的字符数组,例如'234'变为['2', '3', '4']
  let num = str.split('')
  // 保存输入的数字对应的字母数组,例如 '23'=>['abc', 'def']
  let code = []
  num.forEach(item => {
    // 这个判断不能少,否则如果用户输入0或者1那么结果就不对了
    if (map[item]) {
      code.push(map[item])
    }
  })
  const comb = (code) => {
    let tmp = []
    for (let i = 0, il = code[0].length; i < il; i++) {
      for (let j = 0, jl = code[1].length; j < jl; j++) {
        tmp.push(`${code[0][i]}${code[1][j]}`)
      }
    }
    code.splice(0, 2, tmp)
    if (code.length <= 1) {
      return code[0]
    } else {
      return comb(code)
    }
  }
  console.log(comb(code))
}

f('234')

为什么要在 comb(code) 前面加 return 呢?这个理解上不如上面的例子好理解,好像并不需要加 return,其实不是这样的。

还是先写一下执行顺序,

第一次: comb(['abc', 'def', 'ghi']) => 
第二次: comb([['ad', 'ae', 'af', 'bd', 'be', 'bf', 'cd', 'ce', 'cf'], 'ghi'])  

第二次执行 comb 函数时,在执行完 for 循环和 splice 之后 code 的长度就为 1 了,此时返回 [ 'aeg', 'aeh','aei','adg','adh','adi','afg','afh','afi','beg','beh','bei','bdg','bdh', 'bdi','bfg','bfh','bfi', 'ceg', 'ceh','cei', 'cdg' , 'cdh', 'cdi', 'cfg','cfh', 'cfi' ].

注意这是第二次调用 comb 函数返回的结果,如果加了 return comb()第一次调用返回的结果是第二次comb函数执行的结果,如果没有 return,那么第一次调用 comb 的结果就是 undefined。

在递归中满足终止条件返回的值都是最后一次调用递归函数执行的结果,最内层递归函数的执行结果要想返回到最外层递归函数,只有每层递归函数都加上 return 之后才可以。

总而言之,只要递归函数想要最后返回一个结果那么每层的递归函数必须加上 return,这样才能将内层函数的结果返回到最外层递归函数。

接下来再来说一下为什么可以用递归?用户输入的数字字符串中数字的个数未知,但是总是可以将两个字母字符串之间进行组合(大问题和小问题的解决方法是一致的),先进行两个字母字符串之间组合,如果有3个字母字符串,那么就将前两个组合后的结果替换数组的前两项(用到了上一次递归的结果)然后再和之前的第三个字母字符串进行组合。多余 3 个字母字符串时同理。

总结一下

1)只要大问题和小问题的解决方法是一致的,并且用到了上一次的结果,那么就可以考虑使用递归。

2)只要递归函数要返回一个结果,那么每次调用递归函数时前面必须加一个 return,这样才能将内层函数的返回结果返回到最外层函数。

3)最外层函数的返回结果是所有的内层函数执行结束后的返回结果。内层函数是外层函数的执行内容,当它们执行完后最终返回的结果就是外层函数的执行结果。

posted on 2020-04-09 16:00  前端旧约  阅读(217)  评论(0编辑  收藏  举报

导航