递归与递推的区别

刚才在IBM DW上看到这篇《JavaScript 技巧与高级特性》,其中关于arguments.callee的部分有一个用递归来

求斐波那契数列的例子,简化一下是这样的:
//经典递归
function fibonacci(n) {
return (function(n) {
if (n == 1 || n == 2)
return 1;
return arguments.callee(n - 1) + arguments.callee(n - 2);
})(n);
}

fibonacci(4); //result: 3
fibonacci(5); //result: 5
fibonacci(10); //result: 55

这种教科书式的写法出镜率很高,在很多文章里都可以看到,但是速度也特别慢,曾经看到过有些人就拿这种例子来说明“递归的效率低”或者“用javascript做函数式编程效率低”,然后给出迭代的写法……

更新:我今天老老实实的读了SICP的第一章之后发现书中对这个问题其实有很严谨的解释,为了防止自己被骂成民科,赶紧修正了一些说法,加了删除线的文字都是有错误的,新增加的文字用粗体return。

其实这个方法速度慢并不是函数式编程(FP)的错,首先要把词义弄清楚,真正的数学意义上的“递归”(recursive)包含了“递推”(recurrence)和“回归”(regression)的过程,在程序执行的过程中,“递归”(recursive)指的是一种方法,把大的复杂的问题分解成更小更简单的问题,逐级分解下去,直到问题的规模小到可以直接求解,然后再逐级向上回溯直到解决最初的问题,用程序来实现这种算法的时候至少包含一次以上的递推执行过程,效率当然比不上直接作一次迭代。递归的计算过程(recursive process)包含了两个阶段,先逐级扩展(expansion),构造起一个由被推迟的操作组成的链条(会被解释器保存在堆栈里),然后在收缩(contraction)阶段逐级回溯执行那些操作。随着递归计算步骤的增多,这种方法消耗的资源会越来越大,而且会包含越来越多的冗余操作,上面那个求斐波那契数列的例子(在SICP里被称作“树形递归”)在这方面问题尤其严重,因为它的计算步骤会随着参数而指数性的增长。

引用SICP上的图解:


而在编程里常说的递归其实就是简单的指“自己调用自己”的过程,指的是一种语法形式,而不是计算过程,在SICP里使用“递归过程”(recursive procedure)这个词来称呼,表示“一个过程的定义中引用了该过程本身”,在FP里就是一个函数把状态作为参数反复调用自己,来实现迭代的效果,所以未必需要递推一次以上。,用递归过程也可以产生出迭代计算过程(iterative process,迭代计算过程中消耗的资源是一个常量),递归==迭代,这个表达式不仅在lisp,Erlang这类FP语言里成立,在javascript里也一样。

比如那个求斐波那契数列的例子就可以用尾递归:
//尾递归
function fibonacci(n) {
return (function(n1, n2, i) {
return ( i < n ) ? arguments.callee(n2, n1+n2, i+1) : n1;
})(1,1,1);
}

跟这样的迭代方法是完全等价的:
//等价的循环
function fibonacci(n) {
var n1 = n2 = s = i = 1;
for(; i<n; i++){
s = n1 + n2;
n1 = n2;
n2 = s;
}
return n1;

}

都是从数列的起始处开始递推,区别只是:在迭代方法里是把每两个相邻的数相加的和保存在循环体外部的局部变量里,在尾递归方法中是把这个和作为参数传给下一次函数调用。

附带说一下,“尾递归”(Tail Recursion)指的是把计算过程集中在函数递归调用的最后一次把每次函数递归调用中的所有运算结果或操作都逐步传递到最末尾一次的函数调用,FP语言在编译/解释的时候都会把尾递归优化成一次直接的运算,而在javascript引擎里就算没有优化,至少也可以在每次调用过程中不留下任何痕迹,可以像普通的循环语句那样线性的推算到最后,因此无论速度还是内存消耗,都跟普通的迭代方法没有区别。


//分割线

//转载请写上本帖链接和“C++奋斗乐园|C++论坛|算法论坛|ACM/ICPC论坛”

今天在学习递归和动态规划时有点迷糊了,两者无法区别,在网上差了下,总接如下:
首先要清楚,递推就是迭代。

1.递归其实就是利用系统堆栈,实现函数自身调用,或者是相互调用的过程.在通往边界的过程中,都会把单步地址保存下来,知道等出边界,再按照先进后出的进行运算,这正如我们装木桶一样,每一次都只能把东西方在最上面,而取得时候,先放进取的反而最后取出.递归的数据传送也类似.但是递归不能无限的进行下去,必须在一定条件下停止自身调用,因此它的边界值应是明确的.就向我们装木桶一样,我们不能总是无限制的往里装,必须在一定的时候把东取出来.比较简单的递归过程是阶乘函数,你可以去看一下.但是递归的运算方法,往往决定了它的效率很低,因为数据要不断的进栈出栈.这时递推便表现出它的作用了,所谓递推,就是免除了数据进出栈的过程.也就是说,不需要函数不断的向边界值靠拢,而直接从边界出发,直到求出函数值.比如,阶乘函数中,递归的数据流动过程如下:
f(3){f(i)=f(i-1)*i}–>f(2)–>f(1)–>f(0){f(0)=1}–>f(1)–>f(2)–f(3){f(3)=6}
而递推如下:
f(0)–>f(1)–>f(2)–>f(3)
由此可见,递推的效率要高一些,在可能的情况下应尽量使用递推.但是递归作为比较基础的算法,它的作用不能忽视.所以,在把握这两种算法的时候应该特别注意.

2.递归是自顶向下逐步拓展需求,最后自下向顶运算。即由f(n)拓展到f(1),再由f(1)逐步算回f(n)
迭代是直接自下向顶运算,由f(1)算到f(n)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值