您的位置 首页 > 科技

js递归函数的例子 尾递归究竟是好是坏?

js递归函数的例子

js递归函数的例子 尾递归究竟是好是坏?

尾递归究竟是好是坏?

尾递归究竟是好是坏?

概念

递归如果层次太多,就会照成栈溢出异常,因为每调用一次就会新生成一个栈帧,使用这个栈帧保留当前函数的状态值。如果没有必要保存状态值,那么就可以复用栈帧,不会造成栈溢出。

举例

这里以求n的阶乘举例:

正常递归:

假如n=3;那么每一步都需要保留n值以及下一步函数的返回值,所以每次调用都需要创建一个新的栈帧

尾递归:

假如n=3,这里每一次调用是可以复用栈帧的,因为不需要保存状态值。

总结

所以说当递归是在当前栈帧执行完之后,不需要再保留当前栈帧,而是带着当前栈帧的结果,进入到下一栈帧,就可以优化为尾递归,一般尾递归需要满足递归调用是函数体中最后执行的语句。比如阶乘的例子中最后执行的语句是直接调用factorial(n-1, n*result),而不是一个表达式n * factorial(n -1),如果是表达式的话就需要一个栈帧来保留n和factorial(n -1)的结果。

尾递归究竟是好是坏?

相对于递归,尾递归当然是好的,因为尾递归在调用时,直接覆盖当前调用栈,栈不增长(这称为 尾递归优化),但对于循环来说就是不见得了。

理论上 尾递归和循环等价。所以,尾递归能实现的需求,循环一定可以实现,反之亦然。例如,用欧几里得辗转相除法,求取最大公因数(JavaScript),

尾递归:

var gcd = (a, b) => b ? gcd(b, a \% b) : a

循环:

var gcd = (a, b) => {

■while(b) {

■■var t = b

■■b = a \% b

■■a = t

■}

■return a

}

不过,一般来说,尾递归的实现代码更精妙,也更为直观。

对于有些函数式语言,尾递归是必不可少不的,例如:Lisp的循环语句都是用尾递归实现的(scheme)

(define-syntax while

■(syntax-rules ()

■■((_ c expr) ((lambda (loop) (loop loop))(

■■■■■■■■lambda (loop) (if c (begin expr (loop loop))(void)))))))

而有些语言,仅仅将尾递归提供出来作为特殊情况下使用的工具,本身并不依赖,例如:JavaScript。很多的语言本身根本不支持尾递归优化,例如:C。

一般来说,支持 λ表达式的语言,大多支持尾递归优化。

通常情况下,大多数语言在写正式代码中都不建议使用递归,不过在写特殊程序时是允许的,例如:编译器(里面的程序处处是直接或间接递归)。另外,如果是在学习中 或 可行性分析阶段,写一些练习、原型代码,递归是不错的选择。

对于某些函数式语言,尾递归是被鼓励的,例如:Haskell,

gcd r 0 = r

gcd a b = gcd b (a `mod` b)

这时,要尽量将普通递归,化为尾递归,绝大多数情况都可以,例如(JavaScript):

阶乘:

非尾递归:var fact = n => n > 1 ? n * fact(n-1) : 1

尾递归:var fact = (n, total = 1) => n > 1 ? fact(n-1, n * total) : total

斐波那契数列:

非尾递归:var fib = n => n > 2 ? fib(n-1) fib(n-2) : 1

尾递归:var fib = (n, a = 1, b = 1) => n > 2 ? fib(n-1, b, a b) : b

最后,不管是否使用递归(尾递归),我们都需要熟练掌握它,作为一种思维方法而存在。

相关文章