【问题标题】:How to translate the following into a tail recursive procedure?如何将以下内容转换为尾递归过程?
【发布时间】:2011-11-18 17:41:52
【问题描述】:

我有以下数学表达式:

; 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))))))

如何将上述内容转换为尾递归过程?我不习惯函数式编程,所以我有点挣扎。

【问题讨论】:

标签: lisp scheme tail-recursion


【解决方案1】:

(以下代码在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 不再进行非尾递归调用,而是使用带有列表结果的扩展延续调用自身。 (fib3k N k) 的延续 k 使用与 (list (fib (- N 1)) (fib N)) 等效的列表调用。 (因此,如果第一个参数是(- n 1),则继续参数命名为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))

现在我们将创建延续函数的表达式(即lambdas)替换为使用上下文构造函数,并且我们替换了(单个)站点它应用了一个带有新的显式 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

这就是斐波那契的传统迭代版本,除了 apply-context7 通常称为 fib-iter 或类似名称,大多数版本会向上计数而不是向下计数,并希望他们能正确比较,这样他们就不会得到差一个错误。

【讨论】:

    【解决方案2】:

    您正在谈论用于计算斐波那契数的尾递归转换的已建立示例。您可以在 SICP 的 this chapter 找到带有代码示例的出色描述。

    【讨论】:

    • 这很有趣。我编写了“数学”函数来尝试解决 SICP 问题,但没有意识到我最终编写了斐波那契函数。谢谢你,那一章的答案很清楚。
    猜你喜欢
    • 2023-01-13
    • 1970-01-01
    • 1970-01-01
    • 2013-09-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-30
    • 1970-01-01
    相关资源
    最近更新 更多