JS算法之递归

含义

        递归函数是指能够直接或间接调用自身的方法或函数。

// 直接
function do() {
    do();
}

// 间接
function do() {
    do2();
}
function do2() {
    do()
}

        每个递归函数必须有基线条件(即停止点,一个不再递归调用的条件。)否则将无限递归下去。 因此有一句编程的名言是:“要理解递归,首先要理解递归”。

function understandRecursion(doIunderstandRecursion) {
  const recursionAnswer = confirm('Do you understand recursion?'); // function logic
  if (recursionAnswer === true) { // base case or stop point
    return true;
  }
  understandRecursion(recursionAnswer); // recursive call
}

例子

        迭代阶乘便是迭代函数应用的一个很好的例子。

        n!=n * (n-1) * (n-2) ... * 1

function factorial(n) {
    // 基线条件
    if (n  <= 1) {
        return 1
    }
   return n * factorial(n-1)
}

栈的顺序 

        理解递归,需要理解递归时函数执行的调用栈顺序。

        当我们执行factorial(3)的时候:

        执行步骤为:factorial(3) => 3 *  factorial(2) => 2 * factorial(1) 

        此时函数的执行栈完毕,开始弹出调用栈

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

        我们可以通过浏览器的开发者工具进行观察:

         可以看到我们断点的位置在于n为1的时候。此时调用栈里有三个factorial函数。

      

         继续往下走,此时n=2。此时剩下两个factorial函数。n=1的factionrial函数已经回调完毕(返回1)。

         回调factionrial(1)

Js调用栈大小限制

        如果忘记加上基线条件,递归函数并不会无限地执行下去。当调用栈堆叠到一定限度。浏览器就会抛出错误。也就是所谓的栈溢出错误。

        这个限度是由浏览器自身进行限制的。我们可以通过函数进行测试。

let i = 0;
function recursiveFn() {
  i++;
  recursiveFn();
}

try {
  recursiveFn();
} catch (ex) {
  console.log('i = ' + i + ' error: ' + ex);
}

     Edge超限次数为13903 次

        这个数值根据操作系统和浏览器不同,会有差异。

        ES6有尾调用优化。也就是说如果函数内的最后一个操作是调用函数。会通过“跳转指令”而不是“子程序调用”来控制。也就是说,在ES6中,递归函数可能不受栈溢出限制。因此,具有停止递归的基线条件很重要。

解决斐波那契数列

        斐波那契数列是一个由0、1、1、2、3、5、8、13、21、34等数组成的序列。

         上图是直观上的规律。而抽象出计算机数学规律为:

  •         数列(组)的下标0对应0
  •         数列下标1对应1
  •         数列下标n(n>1)对应下标(n-1)值和下标(n-2)值的和。即value(n) = value(n-1) + value(n-2)

        通过数学规律我们可以发现,只要我们特别处理value(0)和value(1)的返回值。其他的都可以交给迭代函数去累加处理。

        为了减少迭代次数,我们再优化一下规律:

  •         数列(组)的下标0对应0
  •         数列下标1对应1
  •         数列下标2对应1
  •         数列下标n(n>2)对应下标(n-1)值和下标(n-2)值的和。即value(n) = value(n-1) + value(n-2)
function fibonacci(n) {
    if (n===0) {return 0}
    if (n<=2)  {return 1}
    return fibonacci(n-1) + fibonacci(n-2)
}

         调用顺序如图。从左树到右树依次遍历过去。

        fibonacci(5) -> fibonacci(4) -> fibonacci(3) -> fibonacci(2) -> fibonacci(1) ->弹出调用栈到fibonacci(3) -> fibonacci(2) -> f弹出调用栈到fibonacci(4) -> ibonacci(3) -> fibonacci(2) -> fibonacci(1)

记忆斐波那契数列

        记忆化是一种保存前一个结果的值的优化技术。类似于缓存。比如上面的fibonacci(5)里,fibonacci(3)被计算了两次。若将它结果储存下来,便可以少计算一次了。

function fibonacciMemory(n) {
    const memoryResult = [0,1,1];
    const fibonacci = (n) => {
        if (memoryResult[n] != null) return memoryResult[n];
        return memoryResult[n] = fibonacci(n-1) + fibonacci(n-2)
    }
    return fibonacci(n)
}

用迭代去实现

export function fibonacciIterative(n) {
  if (n < 1) { return 0; }
  let fibNMinus2 = 0;
  let fibNMinus1 = 1;
  let fibN = n;
  for (let i = 2; i <= n; i++) {
    fibN = fibNMinus1 + fibNMinus2;
    fibNMinus2 = fibNMinus1;
    fibNMinus1 = fibN;
  }
  return fibN;
}

 

迭代递归性能对比

        迭代的版本比递归的版本快很多,所以这表示递归更慢。但是,再看看三个不同版本的归版本更容易理解,需要的代码通常也更少。另外,对一些算法来说,迭代的解法可能不且有了尾调用优化,递归的多余消耗甚至可能被消除。
        所以,我们经常使用递归,因为用它来解决问题会更简单

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值