我有以下数学表达式:
; f(n) = f(n - 1) + f(n - 2) where n >= 2
; f(n) = n where n < 2`
我将其转换为正常的递归LISP调用:
(define (f n)
(cond ((< n 2) n)
(else (+ (f (- n 1))
(f (- n 2))))))
我如何将上述内容转换为尾递归过程?我不习惯功能编程,所以我有点挣扎。
答案 0 :(得分:5)
(以下代码在Racket中编写和测试。)
从天真的版本开始:
;; fib : nat -> nat
(define (fib n)
(cond [(= n 0) 0]
[(= n 1) 1]
[else (+ (fib (- n 1)) (fib (- n 2)))]))
在我们开发新版本时,我们可以使用test
来查看它们是否与初始fib
一致(至少在数字0到9上)。
;; test : (nat -> nat) -> boolean
;; Check that the given function agrees with fib on 0 through 9
(define (test f)
(for/and ([i (in-range 10)])
(= (f i) (fib i))))
首先,启用其他所有内容的关键观察是,当我们计算(fib N)
时,我们计算了(fib (- N 1))
...但我们将其丢弃,因此我们必须稍后重新计算它。这就是天真fib
是指数时间的原因!我们可以通过保留它来做得更好,例如使用返回列表的辅助函数:
;; fib2list : nat -> (list nat nat)
;; (fib2list N) = (list (fib (- N 1)) (fib N))
(define (fib2list n)
(cond [(= n 1) (list 0 1)]
[else (let ([resultN-1 (fib2list (- n 1))])
(let ([fibN-2 (first resultN-1)]
[fibN-1 (second resultN-1)])
(list fibN-1
(+ fibN-2 fibN-1))))]))
;; fib2 : nat -> nat
(define (fib2 n)
(cond [(= n 0) 0]
[else (second (fib2list n))]))
(test fib2) ;; => #t
fib2list
函数在1处停止,因此fib2
将0视为特殊(但无趣)的情况。
我们可以在连续传递样式(CPS)中重写它以使其尾递归:
;; fib3k : nat ((list nat nat) -> nat) -> nat
(define (fib3k n k)
(cond [(= n 1) (k (list 0 1))]
[else (fib3k (- n 1)
(lambda (resultN-1)
(let ([fibN-2 (first resultN-1)]
[fibN-1 (second resultN-1)])
(k (list fibN-1
(+ fibN-2 fibN-1))))))]))
;; fib3 : nat -> nat
(define (fib3 n)
(cond [(= n 0) 0]
[else (fib3k n (lambda (resultN)
(let ([fibN-1 (first resultN)]
[fibN (second resultN)])
fibN)))]))
(test fib3) ;; => #t
现在,fib3k
不是进行非尾递归调用,而是使用扩展的继续调用自身来获取列表结果。使用等同于k
的列表调用(fib3k N k)
的延续(list (fib (- N 1)) (fib N))
。 (因此,如果第一个参数是(- n 1)
,则continuation参数名为resultN-1
,等等。)
要启动所有内容,我们提供一个初始延续,其结果为resultN
;第二个元素等于(fib N)
,所以我们返回。
当然,没有理由将包装内容列为清单;我们可以让延续有两个参数:
;; fib4k : nat (nat nat -> nat) -> nat
(define (fib4k n k)
(cond [(= n 1) (k 0 1)]
[else (fib4k (- n 1)
(lambda (fibN-2 fibN-1)
(k fibN-1
(+ fibN-2 fibN-1))))]))
;; fib4 : nat -> nat
(define (fib4 n)
(cond [(= n 0) 0]
[else (fib4k n (lambda (fibN-1 fibN) fibN))]))
(test fib4) ;; => #t
请注意,程序中只有两个变种的延续 - 它们对应于代码中出现的两次lambda
。这是最初的延续,并且有一种扩展现有延续的方法。使用这种观察,我们可以将延续函数转换为上下文数据结构:
;; A context5 is either
;; - (initial-context)
;; - (extend-context context5)
(struct initial-context ())
(struct extend-context (inner))
现在我们使用上下文构造函数替换创建 continuation functions (即lambda
s)的表达式,然后我们替换(单个) )使用新的显式apply-context5
函数应用延续函数的站点,该函数执行先前由两个lambda
表达式完成的工作:
;; fib5ctx : nat context5 -> nat
(define (fib5ctx n ctx)
(cond [(= n 1) (apply-context5 ctx 0 1)]
[else (fib5ctx (- n 1)
(extend-context ctx))]))
;; apply-context5 : context5 nat nat -> nat
(define (apply-context5 ctx a b)
(match ctx
[(initial-context)
b]
[(extend-context inner-ctx)
(apply-context5 inner-ctx b (+ a b))]))
;; fib5 : nat -> nat
(define (fib5 n)
(cond [(= n 0) 0]
[else (fib5ctx n (initial-context))]))
(test fib5) ;; => #t
(当编译器这样做时,他们称之为去功能化或闭包转换,他们这样做是为了将间接跳转变为直接跳转。)
此时,context
数据类型显然非常无聊。事实上,它在代数上等同于自然数! (自然数是零或自然数的后继。)因此,让我们只更改上下文数据类型以使用自然数而不是一些堆分配结构。
;; A context6 is just a natural number.
;; fib6ctx : nat context6 -> nat
(define (fib6ctx n ctx)
(cond [(= n 1) (apply-context6 ctx 0 1)]
[else (fib6ctx (- n 1)
(+ ctx 1))]))
;; apply-context6 : context6 nat nat -> nat
(define (apply-context6 ctx a b)
(cond [(= ctx 0)
b]
[else
(apply-context6 (- ctx 1) b (+ a b))]))
;; fib6 : nat -> nat
(define (fib6 n)
(cond [(= n 0) 0]
[else (fib6ctx n 0)]))
(test fib6) ;; => #t
但现在显而易见的是fib6ctx
只计算ctx
,因为它将n
计为1.特别是:
(fib6ctx N M) = (fib6ctx 1 (+ N M -1))
= (apply-context6 (+ N M -1) 0 1)
等等
(fib6ctx N 0) = (apply-context6 (+ N -1) 0 1)
所以我们可以完全摆脱fib6ctx
。
;; apply-context7 : nat nat nat -> nat
(define (apply-context7 ctx a b)
(cond [(= ctx 0)
b]
[else
(apply-context7 (- ctx 1) b (+ a b))]))
;; fib7 : nat -> nat
(define (fib7 n)
(cond [(= n 0) 0]
[else (apply-context7 (- n 1) 0 1)]))
(test fib7) ;; => #t
这是Fibonacci的传统迭代版本,除了apply-context7
通常被称为fib-iter
或类似的东西,并且大多数版本都计数而不是下来并希望他们得到正确的比较所以他们不要不会出现一个错误。
答案 1 :(得分:3)
您正在讨论用于计算Fibonacci数的尾递归变换的已建立示例。您可以在SICP的this chapter找到包含代码示例的精彩描述。