尾递归和递归的区别

686 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

  • 尾递归:进入下一个函数不再需要上一个函数的环境了,得出结果以后直接返回。
  • 非尾递归:下一个函数结束以后此函数还有后续,所以必须保存本身的环境以供处理返回值。

一、递归

  • 递归就是在过程或函数里调用自身
  • 在使用递归时,必须有一个明确的递归结束条件,我们称之为递归的出口。

使用递归解决的问题

  • 数据的定义是按照递归定义的。(斐波那契数列)
  • 问题的解需要按照递归实现。(回溯)
  • 数据结构是按递归定义的。(树)

缺点:在递归调用的过程中系统为每一层的返回点局部变量开辟了栈来存储,因此递归次数过多容易造成栈溢出。

二、尾递归

​ 尾递归就是从最后开始计算,每递归一次就算出相应的结果,也就是说,函数调用出现再调用者的尾部。因为是尾部,所以根本没有必要去保存任何局部变量,直接让被调用的函数返回时越过调用者,返回到调用者的调用者中去。

​ 尾递归就是把当前的运算结果(或路径)放在参数里传给下层函数,深层函数所面对的不是越来越简单的问题,而是越来越复杂的问题,因为参数里带有前面若干步的运算路径。

三、递归和尾递归的区别

尾递归和一般递归的不同点在于对内存的占用

  • 尾递归比普通递归多一个参数,这个参数是上一次调用函数得到的结果
  • 所以,关键点在于尾递归每次调用都在收集结果,避免了普通递归不收集结果只能依次展开(消耗内存)的坏处。

实际上,尾递归只是一种形式。这种形式表达的代码可以被某些编译器优化。

尾递归的特殊形式决定了这种递归代码在执行过程中是可以不需要回溯的(通常递归都是需要回溯的)。如果编译器针对尾递归形式的代码做了这种优化,就可能把原本需要线性复杂度的内存空间执行过程改为用常数复杂度的空间完成。

四、优化点

尾递归主要是针对栈内存空间的优化。这个优化是 O(n)O(1) 的;

对于时间的优化,实际上是由于对空间的优化导致内存分配的工作减少所产生的,是一个常数优化,不会带来质的变化。

五、代码示例

实现一个 “计算斐波那契数列第n项” 函数

  • 第一版:最直接的递归实现(树形递归)
const fib = (n) => {
  if(n <= 2) return 1;
  return fib(n - 1) + fib(n - 2);
}
  • 第二版:迭代(循环)实现
const fib = (n) => {
  let a = 1, b = 1;
  for(let i=0; i<n; i++) {
    let a_other = b, b_other = a+b;
    a = a_other;
    b = b_other;
  }
  return a
}
  • 第三版:线性递归
const fib = (n) => {
  const fib_iter = (a, b, n) => {
    if(n <= 1) return 1;
    return a + fib_iter(b + a+b, n-1); // 这里做个一个计算
  }
  return fib_iter(1, 1, n);
}
  • 第四版:尾递归实现
const fib = (n) => {
  const fib_iter = (a, b, n) => {
    if(n === 0) return a;
    return fib_iter(b, a+b, n-1); // 这里没有做计算,而是直接将结果返回了
  }
  return fib_iter(1, 1, n);
}

从第三版和第四版可以看出,尾递归就是把一个依赖上一层环境(上下文)的递归(第三版),转变为一个不依赖上一层环境的递归(第四版),转变的方法就是把需要用到的环境通过参数传递给下一层。