美文网首页
Scheme学习笔记(一)——尾递归

Scheme学习笔记(一)——尾递归

作者: yuchanns | 来源:发表于2019-02-10 00:30 被阅读9次

欢迎访问我的博客原文地址
本文为SCIP课堂作业思考总结。

The Scheme
Programming LanguageThe Scheme Programming Language

尾递归的定义

尾递归是函数式编程中,递归函数的一种优化手段。

根据Wikipedia的定义:

In computer science, a tail call is a subroutine call performed as the final action of a procedure. If a tail call might lead to the same subroutine being called again later in the call chain, the subroutine is said to be tail-recursive, which is a special case of recursion.
在一个程序中,如果一个子程序在该程序的最后一步被调用,就称之为尾调用。如果这个子程序在执行末尾再次调用了自身,就可以称作是尾递归。尾递归是递归调用的一种特殊形式。

普通递归的实现

下面以斐波那契数列为例。

斐波那契数列(Fibonacci sequence)指的是这样一个数列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368........
这个数列从第3项开始,每一项都等于前两项之和。

根据SCIP第一节视频我们可知,普通递归实现费氏数列的方法如下:

; 普通递归
(define (fib n)
    (if (< n 2)
        n
        (+ (fib (- n 1))
            (fib (- n 2)))))

实现起来十分简单,在函数末尾不停调用自身即可。然而我们分析函数执行模式就会发现,这样的递归调用开枝散叶,是树状的——在执行过程中会如滚雪球一样越滚越大。

fib(100)
fib(98)       +       fib(99)
fib(96) + fib(97)     fib(98) + fib(97)
...

在上面的执行模式分析中,我们可以看到,每一次递归调用执行,它都会进行一次相加操作,而相加操作的参数又来自于递归调用,因此需要等待递归调用返回结果,并且每次调用结果都需要入栈保存。

其中很多地方的计算是重复浪费的。当n非常大的时候(实际上n=50就很明显),如果程序按从左到右的顺序执行一遍所有的树状计算,消耗的时间难以想象,而且还会出现栈溢出现象。

这时候就需要使用尾递归进行优化了,即每次进行递归调用的时候,直接把当前的结果当做参数放入下次递归调用,无需等待返回结果,也无需保存所有的调用结果。
那么如何实现尾递归呢?

尾递归的分析与实现

首先分析一下费氏数列的规律:

; fib(100)
; fib(100 - 2) + fib(100 - 1) => fib(98) + fib(99)
; fib(100 - 3) + 2 * fib(100 - 2) => fib(97) + 2 * fib(98)
; 2 * fib(100 - 4) + 3 * fib(100 - 3) => 2 * fib(96) + 3 * fib(97)
; ...
;
; fib(4)
; fib(2) + fib(3)
; fib(1) + 2 * fib(2)
; 2 * fib(0) + 3 * fib(1)

根据上述例子,我们可以归纳出以下规律:

  • 当n大于等于2时,fib(n) = fib(n - 2) + fib(n - 1) = fib(n - 3) + 2 * fib(n - 2) = 2 * fib(n - 4) + 3 * fib(n - 3) = ... = a * fib(0) + b * fib(1)
  • fib(n)的调用次数为n-1次

其中ab的含义为:a是上一轮递归中的b,而b是上一轮递归中的a与b之和。b也是个斐波那契数。并且我们可以知道,第一轮递归的a和b必定都为1。
以这个规律,我们可以得出下面的代码片段:

; 尾递归

(define (fib_tail a b n)
    (if (> 2 n)
        a
        (fib_tail b (+ a b) (- n 1))))
(define (fib n)
    (fib_tail 1 1 n))
(display (fib 1000))
(display #\newline)
(time (fib 1000))
(display #\newline)

运行代码可以轻松得出fib_tail(1000)的值,在我的电脑上消耗时间如下:

(time (fib 1000))
    no collections
    0.000100112s elapsed cpu time
    0.000099000s elapsed real time
    57744 bytes allocated

利用scheme的named let语法糖(我在《初识scheme》中提到过),我们可以把代码片段写得更为赏心悦目:

; 使用named let定义函数并循环自调用
(define (fib_new number)
    (let fib_tail_new((a 1) (b 1) (n number))
        (if (> 2 n)
            a
            (fib_tail_new b (+ a b) (- n 1)))))
(display (fib_new 1000))
(display #\newline)

总结

尾递归的中心思想在于:仅在程序的末尾进行调用,并且不等待返回结果

相关文章

网友评论

      本文标题:Scheme学习笔记(一)——尾递归

      本文链接:https://www.haomeiwen.com/subject/tdhteqtx.html