尾递归(转)

add by zhj: 尾递归其实跟循环在代码形式上非常像,尾递归会同时用到反推和正推,由n->n-1是反推,由acc1, acc2 = acc2, acc1+acc2是正推。参见本文最后,分别用循环和尾递归实现Fibonacci数列。

原文:http://www.nowamagic.net/librarys/veda/detail/2325

尾递归(tail recursive),看名字就知道是某种形式的递归。简单的说递归就是函数自己调用自己。那尾递归和递归之间的差别就只能体现在参数上了。

尾递归wiki解释如下:

尾递归是指在递归函数中,递归调用返回的结果总被直接返回(即return),则称为尾部递归。尾部递归的函数有助将算法转化成函数编程语言,而且从编译器角度来说,亦容易优化成为普通循环。这是因为从电脑的基本面来说,所有的循环都是利用重复移跳到代码的开头来实现的。如果有尾部归递,就只需要叠套一个堆栈,因为电脑只需要将函数的参数改变再重新调用一次。利用尾部递归最主要的目的是要优化,例如在Scheme语言中,明确规定必须针对尾部递归作优化。可见尾部递归的作用,是非常依赖于具体实现的。

我们还是从简单的斐波那契开始了解尾递归吧。

用普通的递归计算Fibonacci数列:

#include "stdio.h"
#include "math.h"

int factorial(int n);

int main(void)
{
    int i, n, rs;

    printf("请输入斐波那契数n:");
    scanf("%d",&n);

    rs = factorial(n);
    printf("%d \n", rs);

    return 0;
}

// 递归
int factorial(int n)
{
    if(n <= 2)
    {
        return 1;
    }
    else
    {
        return factorial(n-1) + factorial(n-2);
    }
}

运行结果如下:

请输入斐波那契数n:20
6765

Process returned 0 (0x0)   execution time : 3.502 s
Press any key to continue.

在i5的CPU下也要花费 3.502 秒的时间。

下面我们看看如何用尾递归实现斐波那契数。

#include "stdio.h"
#include "math.h"

int factorial(int n);

int main(void)
{
    int i, n, rs;

    printf("请输入斐波那契数n:");
    scanf("%d",&n);

    rs = factorial_tail(n, 1, 1);
    printf("%d ", rs);

    return 0;
}

int factorial_tail(int n,int acc1,int acc2)
{
    if (n < 2)
    {
        return acc1;
    }
    else
    {
        return factorial_tail(n-1,acc2,acc1+acc2);
    }
}

运行结果如下:

请输入斐波那契数n:20
6765
Process returned 0 (0x0)   execution time : 1.460 s
Press any key to continue.

快了一倍有多。当然这是不完全统计,有兴趣的话可以自行计算大规模的值,这里只是介绍尾递归而已。

我们可以打印一下程序的执行过程,函数加入下面的打印语句:

int factorial_tail(int n,int acc1,int acc2)
{
    if (n < 2)
    {
        return acc1;
    }
    else
    {
        printf("factorial_tail(%d, %d, %d) \n",n-1,acc2,acc1+acc2);
        return factorial_tail(n-1,acc2,acc1+acc2);
    }
}

程序运行结果:

请输入斐波那契数n:10
factorial_tail(9, 1, 2)
factorial_tail(8, 2, 3)
factorial_tail(7, 3, 5)
factorial_tail(6, 5, 8)
factorial_tail(5, 8, 13)
factorial_tail(4, 13, 21)
factorial_tail(3, 21, 34)
factorial_tail(2, 34, 55)
factorial_tail(1, 55, 89)
55
Process returned 0 (0x0)   execution time : 1.393 s
Press any key to continue.

从上面的调试就可以很清晰地看出尾递归的计算过程了。acc1就是第n个数,而acc2就是第n与第n+1个数的和,这就是我们前面讲到的“迭代”的精髓,计算结果参与到下一次的计算,从而减少很多重复计算量。

fibonacci(n-1,acc2,acc1+acc2)真是神来之笔,原本朴素的递归产生的栈的层次像二叉树一样,以指数级增长,但是现在栈的层次却像是数组,变成线性增长了,实在是奇妙,总结起来也很简单,原本栈是先扩展开,然后边收拢边计算结果,现在却变成在调用自身的同时通过参数来计算。

add by zhj:下面用循环和尾递归分别实现斐波那契数列(Python)

可以发现,两种方法的代码非常像,我个人建议对于尾递归用循环实现更好,代码更易读,也更节省内存。

方法一:用循环实现

def Fibonacci(n):
    x = 0
    y = 1

    while n:
        x, y = y, x+y
        n -= 1

    return x

方法二:用尾递归实现

def Fibonacci(n, acc1, acc2):
    if n < 2:
        return acc1
    else:
        acc1, acc2 = acc2, acc1+acc2        n -= 1
        return Fibonacci(n, acc1, acc2)

小结

尾递归的本质是:将单次计算的结果缓存起来,传递给下次调用,相当于自动累积。

在Java等命令式语言中,尾递归使用非常少见,因为我们可以直接用循环解决。而在函数式编程语言中,尾递归却是一种神器,要实现循环就靠它了。

很多人可能会有疑问,为什么尾递归也是递归,却不会造成栈溢出呢?因为编译器通常都会对尾递归进行优化。编译器会发现根本没有必要存储栈信息了,因而会在函数尾直接清空相关的栈。

延伸阅读

此文章所在专题列表如下:

  1. 漫谈递归:递归的思想
  2. 漫谈递归:递归需要满足的两个条件
  3. 漫谈递归:字符串回文现象的递归判断
  4. 漫谈递归:二分查找算法的递归实现
  5. 漫谈递归:递归的效率问题
  6. 漫谈递归:递归与循环
  7. 漫谈递归:循环与迭代是一回事吗?
  8. 递归计算过程与迭代计算过程
  9. 漫谈递归:从斐波那契开始了解尾递归
  10. 漫谈递归:尾递归与CPS
  11. 漫谈递归:补充一些Continuation的知识
  12. 漫谈递归:PHP里的尾递归及其优化
  13. 漫谈递归:从汇编看尾递归的优化
时间: 2024-10-12 08:30:34

尾递归(转)的相关文章

尾递归

通过阶乘计算来认识尾递归.阶乘可以用下面的表达式来描述: n!=n*(n-1)*(n-2)…3*2*1 根据上面的表达式我们可以概括出下面的算法来计算阶乘: n!=n*(n-1)! public int Factorial(int number) { if (number == 1) { return 1; } var temp = number * Factorial(number - 1); return temp; } 函数调用: var calculator=new Calculator

尾递归和线性递归

1.递归的定义 函数直接或间接的调用自己 使用递归时,必须有明确的结束递归的条件 2.递归的适用场合 数据的定义按照递归定义(比如求n!) 问题的解法适用于使用递归 数据的结构是按递归定义的(比如二叉树) 3.线性递归 也就是普通递归,下一次递归数据的计算要依赖于上一次递归的结果和参数,当数据量较小时执行效率与尾递归几乎没区别,但当数据量较大,迭代次数较多时,由于每次递推都要在内存中开辟一个栈空间,用来存储上次递推的结果和参数,这样的算法将导致严重的内存开销,甚至造成内存溢出,抛出java.la

尾递归 - 以斐波那契数列为例说明

尾递归 前言:今天上网看帖子的时候,看到关于尾递归的应用(http://bbs.csdn.net/topics/390215312),大脑中感觉这个词好像在哪里见过,但是又想不起来具体是怎么回事.如是乎,在网上搜了一下,顿时豁然开朗,知道尾递归是怎么回事了.下面就递归与尾递归进行总结,以方便日后在工作中使用. 1.递归 关于递归的概念,我们都不陌生.简单的来说递归就是一个函数直接或间接地调用自身,是为直接或间接递归.一般来说,递归需要有边界条件.递归前进段和递归返回段.当边界条件不满足时,递归前

Gcc 优化选项 与尾递归优化

今天做高性能计算机系统的作业的时候,发现gcc中的优化选项有很多应用 . 例如对于C源码: #include <stdio.h> #include <stdlib.h> int main() { int x[101],y[101]; int a,i; a = 5; for(i=0;i<=100;i++) { x[i] = i+1; y[i] = i; } for(i=100; i>=0; i--) y[i] += a*x[i]; return 0; } 1.直接用gcc

JVM原生不支持尾递归优化,但是Scala编译器支持

The JVM doesn't support TCO natively, so tail recursive methods will need to rely on the Scala compiler performing the optimization.----------"Scala in Depth" 3.5.2 Jvm本身是不支持尾递归优化得,需要编译器支持,而Java编译器不支持,但是Scala支持.写一个简单的计算1到n的和的递归算法验证一下. public cla

对SNL语言的解释器实现尾递归优化

对于SNL语言解释器的内容可以参考我的前一篇文章<使用antlr4及java实现snl语言的解释器>.此文只讲一下"尾递归优化"是如何实现的--"尾递归优化"并不是一个语言实现必须要做的,但这是一个比较有趣的东西,所以我还是想拿来讲一讲. 在前一篇文章中有一个例子: program recursion    procedure f(integer d);    begin        write(d);        f(d + 1)    endbe

Java 递归、尾递归、非递归 处理阶乘问题

n!=n*(n-1)! import java.io.BufferedReader; import java.io.InputStreamReader; /** * n的阶乘,即n! (n*(n-1)*(n-2)*...1). * 0!为什么=1,由于1!=1*0!.所以0!=1 * * @author stone * @date 2015-1-6 下午18:48:00 */ public class FactorialRecursion { static long fact(long n) {

从斐波那契开始了解尾递归

尾递归(tail recursive),看名字就知道是某种形式的递归.简单的说递归就是函数自己调用自己.那尾递归和递归之间的差别就只能体现在参数上了. 尾递归wiki解释如下:豪享博娱乐城 尾部递归是一种编程技巧.递归函数是指一些会在函数内调用自己的函数,如果在递归函数中,递归调用返回的结果总被直接返回,则称为尾部递归.尾部递归的函数有助将算法转化成函数编程语言,而且从编译器角度来说,亦容易优化成为普通循环.这是因为从电脑的基本面来说,所有的循环都是利用重复移跳到代码的开头来实现的.如果有尾部归

尾递归做区间合并插入示例

有一区间列表ranges [[0, 2], [4, 6], [8, 10], [12, 14]],按序排列好了的,没有交集.现在有一新范围range_new [4, 9],进行合并. 采用递归思想,可以用range_new依次和ranges中的范围比较 如果range_new是子集,直接返回 如果range_new小于当前范围,左边直接插入 如果range_new大于当前范围,递归ranges右边的范围比较 如果range_new存在交集,求并集,删除当前范围,如果最大值变化,用并集递归rang

递归和尾递归

C允许一个函数调用其本身,这种调用过程被称作递归(recursion).最简单的递归形式是把递归调用语句放在函数结尾即恰在return语句之前.这种形式被称作尾递归或者结尾递归,因为递归调用出现在函数尾部.由于为递归的作用相当于一条循环语句,所以它是最简单的递归形式.递归中必须包含可以终止递归调用的语句!递归的有点在于为某些编程问题提供了最简单的方法,而缺点是一些递归算法会很快耗尽计算机的内存资源.同时,使用递归的程序难于阅读和维护 咨询了一下专家,确实对于"尾递归"的情况,也就是说函