javaScript - 递归、尾递归详解

递归

何为递归?

递归定义是数理逻辑和计算机科学用到的一种定义方式,使用被定义对象的自身来为其下定义(简单说就是自我复制的定义)
递归,就是在运行的过程中不断地调用自己
  a. 递归有两个过程,简单地说一个是递的过程,一个是归的过程
  b. 如果内部没有结束条件,就会形成一个死循环

递归经典案例:阶乘、自然数、斐波那契数和康托尔三元集,我们来看一个阶乘的案例:

先了解一下什么是阶乘:

我们看看阶乘的两种实现方式

1. 迭代(循环)

function factorial(n) {
  let product = 1
  for (let i = 0; i <= n; i++) {
    product *= i
  }
  return product
}
factorial(5) // 120

2. 递归

function factorial(n) {
  if (n <= 1) return 1
  return n * factorial(n - 1)
}
factorial(5) // 120

分析一下,递归运行过程中,执行上下文栈是如何变化的

step1: 函数执行前,上下文栈已经存在一个全局上下文

step2: 开始执行函数,js引擎创建 factorial 的执行上下文,将该执行上下文压(push)入栈中,然后开始执行函数中的代码 - 递的过程

step3: 执行到末尾时,再次调用 factorial,重复step2

step4: 一直到 n <= 1 函数有了返回值 1 才不会产生新的执行上下文,此时栈中已存在6个执行上下文

step5: 执行上下文栈开始出(pop)栈,返回结果 - 归的过程

我们可以借助图片更好的理解这个过程:

 

到这我们可以设想一下,如果n为100、1000、10000时上下文栈中有几个上下文?
对于上下文栈来说,水满则溢,它是有一定容量限制的,当超出了该容量限制,就会发生栈溢出


在不同的浏览器中,上下文栈的空间限制不同,对函数的递归次数限制也不同

优点:代码简洁

缺点:

1、时间和空间的消耗比较大:每一次函数调用,都需要在内存栈中分配空间以保存参数、返回值和临时变量,往栈中压入和弹出数据也都需要时间
2、重复计算:递归的本质时把一个问题分解成两个或多个问题,多个问题存在重叠的部分,即存在重复计算
3、
栈溢出风险


尾递归

通过上面的递归学习,我们不难知道 factorial(n) 是依赖于 factorial(n-1) 的,factorial(n) 只有在得到 factorial(n-1) 的结果之后,才能计算它自己的返回值,因此理论上,在 factorial(n-1) 返回之前,factorial(n),不能结束返回

尾递归的作用就是消除这个限制,也就是对递归的优化,防止爆栈

何为尾递归?

1、每一个函数在调用下一个函数之前,可以将当前自己占用的栈给释放掉,在调用链上可以做到只有一个函数在使用栈,只存在一个调用帧,所以永远不会发生栈溢出

2、简单来说,就是当前的调用先计算出程序的部分结果,在下一次递归调用时将该结果作为参数传递,那么到了最后一次调用时,就能得到程序的最终结果

3、复杂度O(1)

如果我们把递归调用看作一条链:

递归是链头通过逐次调用自身到达链尾,又由链尾开始逐层计算并向上返回,最终从链头获取最终结果。  

尾递归则由链头开始逐层计算部分结果,并向下传递,在链尾获得最终结果。

阶乘尾递归实现

// 1.
// 尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身
// 要做到这一点,需要把函数内部所有用到的中间变量改写为函数的参数
// 缺点:语义不明显
function factorial(n, total) {
  if (n === 1) {
    console.trace() // 打印堆栈跟踪消息
    return total
  }
  return factorial(n - 1, n * total)
}
factorial(5, 1) // 120


// 2.
// 用一个符合语义的函数去调用改写后的尾递归函数
function factorial(n){
  return inner(n, n - 1)
}

function inner(product, n){
  console.log(inner.caller) // 返回调用当前函数的那个函数
  if(n <= 1){
    return product 
  }
  return inner(product * n , n - 1);
}
factorial(5) // 120

注意:

我们使用了尾递归后(下图为 chrome 浏览器),发现调用栈还是会压入多个执行上下文,这是为什么呢?(注:栈最底部的 anonymous,是全局的函数入口)

 

原因

1、目前只有Safari浏览器支持尾递归优化,Chrome和Fiefox都不支持

2、ES6的尾递归优化只在严格模式(strict mode)下开启,正常模式下是无效的(因为在  正常模式下,函数内部有两个变量,可以跟踪函数的调用栈)


注:

  • arguments:返回调用时函数的参数。
  • func.caller:返回调用当前函数的那个函数

    尾调用优化发生时,函数的调用栈会改写,上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效


 

总结

  • 直接递归需慎用,不仅会带来极差的运行效率,还有可能导致浏览器崩溃
  • 尾递归有着与循环同样优秀的计算性能,以及递归的数学表达能力 
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值