【问题标题】:understanding tail recursion 2理解尾递归 2
【发布时间】:2014-06-26 21:06:01
【问题描述】:

最初我发布了一个问题“understanding a tail-recursive vector->list answer”,这是附加问题。我对方案的一般理解真的很模糊。所以我现在还有几个问题:

;;;;;; original code ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define vector->list:rec
 (lambda (v)
  (letrec ((helper
      (lambda (vec r i)
        (if (< i 0) 
            r
            (helper vec (cons (vector-ref v i) r) (- i 1))  ;; Q1
            ))))
    (if (> (vector-length v) 0)  ;; line 9
      (helper v                  ;; line 10 
              (cons (vector-ref v (- (vector-length v) 1)) '()) 
              (- (vector-length v) 2))
      '()))))

Q2) 尾递归,这让我很难理解。我理解他们为什么需要尾递归,基本上他们使用它来避免迭代,所以他们使用帮助程序作为中间例程..所以他们可以避免将每次迭代放入堆栈......这样的事情。和 letrec/lambda 表达式如下:

;;;;;;;;;letrec/lambda express;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (some-procedure...)
  (letrec ((helper (lambda (x) 
               ...
               (if some-test?
                   (helper ...))))) ; recursive call --- Q2-1
       ...
    (helper ...)  ; call to recursive local procedure  ---- Q2-2
  ...))

第 Q2-2 行:为什么这是“局部递归”? “本地”对我来说听起来像是中间例程的递归......这里的中间意味着我的理解......

[我的困惑] 尾递归在整个程序结束之前不应该迭代(调用)本身 - 所以不应该在中间例程中.. = 不应该在里面帮手?根据我到目前为止的理解......助手是用于封装在letrec表达式中的中间例程......?。)所以最后只调用自己。(我的意思......:在letrec之外......?)。

【问题讨论】:

  • 哇。试着把你的问题分解成更小、更简单、更容易理解的问题。还要确保在适用的情况下使用inline code formatting。这很难阅读。
  • 首先阅读this answer。你明白里面的一切吗?
  • “调用递归本地过程”是指调用本地过程,它(过程)是递归的。

标签: scheme tail-recursion letrec


【解决方案1】:

首先,我会稍微改写你的例子:

(define (vector->list-iter v)  
  (let loop ((i (- (vector-length v) 1)) (acc '()))
    (if (< i 0)
        acc
        (loop (- i 1) (cons (vector-ref v i) acc)))))

要查看差异,让我们制作非尾递归版本:

(define (vector->list-rec v)
  (define len (vector-length v))
  (let loop ((i 0))
    (if (>= i len)
        '()
        (cons (vector-ref v i) (loop (+ i 1))))))

Scheme 中没有循环功能。只有递归和不增长栈的递归,因为上一步还有更多的事情要做,叫做尾递归。

因为我们可以以任何方式迭代一个向量(它是 O(1) 访问),我们可以从最后到第一个或从第一个到最后迭代它。由于列表只能从最后到第一个,因此非尾递归版本不会在第一个元素上应用cons,直到它创建了除第一个元素之外的整个列表。这使得 5 元素向量在达到基本情况时具有 5 个延续。如果它是一个大向量,可能会导致堆栈溢出。

第一个示例首先创建由最后一个元素组成的列表,然后在完成后递归。它不需要 cons 任何东西,因为 consing 是在递归之前完成的。不是所有问题都可以这样处理。想象一下,您想复制一个列表。它可以从头到尾迭代,但可以从头到尾构建。如果没有突变或额外的 consing,就无法使此类过程尾递归。

【讨论】:

  • 我试过你的非尾递归版本。然后我得到这个“定义值:不允许分配;无法更改常量:向量->列表”
  • @user1915570 你的 Scheme 实现不喜欢你重新定义库过程。我已将名称更改为后缀,iter 用于尾递归,rec 用于非尾递归。
  • Scheme 中没有do 吗?
  • @WillNess 它是派生的语法。 In the report 你会发现它只是一个宏,它使用尾递归来完成这项工作。我不太喜欢do,更喜欢命名为let,这是相同的,但它不会隐藏递归。就像 CL 中 loop 的魔力一样,你不得不佩服 Scheme 的纯洁性。
  • 我已经阅读了您提供的链接,上面写着“当和除非表达式是派生形式”,但它并没有提到 do。它显示了do 的宏定义,但这可能只是作为描述。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-03-21
  • 1970-01-01
  • 1970-01-01
  • 2018-03-17
  • 1970-01-01
  • 2017-11-11
  • 1970-01-01
相关资源
最近更新 更多