【问题标题】:Given a recursive function, how do I change it to tail recursive and streams?给定一个递归函数,如何将其更改为尾递归和流?
【发布时间】:2012-07-25 16:08:02
【问题描述】:

给定方案中的递归函数,我如何将该函数更改为尾递归,然后如何使用流实现它?以这种方式更改任何功能时,您是否遵循模式和规则?

以这个函数为例,它创建一个从 2-m 开始的数字列表(这不是尾递归吗?)

代码:

(define listupto
  (lambda (m)
    (if (= m 2)
        '(2)
        (append (listupto (- m 1)) (list m)))))

【问题讨论】:

    标签: recursion stream scheme tail-recursion


    【解决方案1】:

    我将首先解释您的示例。它绝对不是尾递归。想想这个函数是如何执行的。每次追加时,您必须先返回并进行递归调用,直到遇到基本情况,然后再向上拉。

    这是你的功能的痕迹:

    (listupto 4)
    | (append (listupto(3)) '4)
    || (append (append (listupto(2)) '(3)) '(4))
    ||| (append (append '(2) '(3)) '(4))
    || (append '(2 3) '(4))
    | '(2 3 4)
    '(2 3 4)
    

    请注意您看到的 V 模式拉入然后退出递归调用。尾递归的目标是将所有调用构建在一起,并且只执行一次。您需要做的是将累加器与您的函数一起传递,这样当您的函数到达基本情况时,您只能进行一个追加。

    这是你的函数的尾递归版本:

    (define listupto-tail
      (lambda (m)
         (listupto m '())))
    
    # Now with the new accumulator parameter!
    (define listupto
       (lambda (m accu)
         (if (= m 2)
            (append '(2) accu)
            (listupto (- m 1) (append (list m) accu)))))
    

    如果我们看到这条踪迹,它会是这样的:

    (listupto 4)
    | (listupto (3) '(4))  # m appended with the accu, which is the empty list currently
    || (listupto (2) '(3 4)) # m appended with accu, which is now a list with 4
    ||| (append '(2) '(3 4))
    '(2 3 4)
    

    注意模式的不同之处,我们不必遍历递归调用。这为我们节省了无意义的处决。尾递归可能是一个难以掌握的概念,我建议看一下here。第 5 章中有一些有用的部分。

    【讨论】:

    • 这与我在单独教程中阅读的内容一致,但更有意义。谢谢!现在,我如何将这个示例更改为流?假设我想要一个无限列表 2-inf,但我不想全部计算(很明显)。这如何/将如何工作?我不需要单独调用保存流状态的对象(即流中的下一个(第一个)项,以及如何计算其余部分)?
    • 我很高兴我的例子有帮助!我认为您应该采用延续传递样式,而不是传递传递累加器,而是传递当前函数的延续。这将建立一个大语句来一次执行。通过这种方式,您可以跟踪函数的当前状态,并随时将其短路(例如使用基本案例或任何其他输入)。 Wikipedia 很好地解释了这个概念,最初是一个很难掌握的概念,但掌握后却是一项绝妙的技术。
    【解决方案2】:

    通常要切换到尾递归形式,您需要转换代码,以便它采用累加器参数来构建结果并用作最终返回值。这通常是您的 main 函数也委托的辅助函数。

    某种形式:

    (define listupto
      (lambda (m)
         (listupto-helper m '())))
    
    (define listupto-helper
       (lambda (m l)
         (if (= m 2)
            (append '(2) l)
             (listupto-helper (- m 1) (append (list m) l)))))
    

    正如 cmets 所指出的,辅助函数可以用一个命名的 let 替换,这显然(还没有做太多/足够的方案!)更惯用(并且正如 cmets 所建议的那样,cons 比创建一个列表和附加。

    (define listupto
      (lambda (n)
        (let loop ((m n) (l '()))
          (if (= m 2)
              (append '(2) l)
              (loop (- m 1) (cons m l))))))
    

    【讨论】:

    • 实际上,命名let 是写这个的惯用方式,但是对于累加器变量 +1。
    • @larsmans 你说得对,看起来确实更整洁。感谢您指出这一点!
    • 还有,但这确实是 OP 代码中的一个错误,(append (list x) y) 习惯性地写成 (cons x y)
    • 这个名字也很混乱。叫listupto,但应该是listfromtwoupto,不从0或1开始似乎很傻。
    • 我同意cons,所以我改变了它。不想更改函数的名称,因为它偏离了问题的重点。
    【解决方案3】:

    您还询问有关流的问题。您可以找到使用的 SICP 样式流,例如herehere 定义了 from-By 流构建器:

     ;;;; Stream Implementation
     (define (head s) (car s))
     (define (tail s) ((cdr s))) 
    
     (define-syntax s-cons
       (syntax-rules () 
         ((s-cons h t) (cons h (lambda () t))))) 
    
     ;;;; Stream Utility Functions
     (define (from-By x s)
       (s-cons x (from-By (+ x s) s)))
    

    此类流的创建依赖于宏,并且必须通过特殊方式访问它们:

     (define (take n s) 
       (cond  ; avoid needless tail forcing for n == 1 !
          ((= n 1) (list (head s)))   ; head is already forced
          ((> n 1) (cons (head s) (take (- n 1) (tail s))))
          (else '())))
    
     (define (drop n s)
       (cond 
          ((> n 0) (drop (- n 1) (tail s)))
          (else s)))
    

    但它们不是持久的,即 takedrop 在每次访问时重新计算它们。使流持久化的一种方法是通过手术改变最后一个访问 cons 单元的尾部关闭:

    (1 . <closure>)
    (1 . (2 . <closure>))
    ....
    

    像这样:

    (define (make-stream next this state)
      (let ((tcell (list (this state))))  ; tail sentinel cons cell
        (letrec ((g (lambda ()
                        (set! state (next state))
                        (set-cdr! tcell (cons (this state) g))
                        (set! tcell (cdr tcell))
                        tcell)))
          (set-cdr! tcell g)
          tcell)))
    
    (define (head s) (car s))
    
    (define (tail s)
      (if (or (pair? (cdr s))
              (null? (cdr s)))
        (cdr s)
        ((cdr s))))
    

    我们现在可以这样使用它

    (define a (make-stream (lambda (i) (+ i 1)) (lambda (i) i) 1))
    ;Value: a
    
    a
    ;Value 13: (1 . #[compound-procedure 14])
    
    (take 3 a)
    ;Value 15: (1 2 3)
    
    a
    ;Value 13: (1 2 3 . #[compound-procedure 14])
    
    (define b (drop 4 a))
    ;Value: b
    
    b
    ;Value 16: (5 . #[compound-procedure 14])
    
    a
    ;Value 13: (1 2 3 4 5 . #[compound-procedure 14])
    
    (take 4 a)
    ;Value 17: (1 2 3 4)
    
    a
    ;Value 13: (1 2 3 4 5 . #[compound-procedure 14])
    

    现在,(make-stream (lambda (i) (list (cadr i) (+ (car i) (cadr i)))) car (list 0 1)) 定义了什么?


    更新:Daniel Friedman 的 1994 年幻灯片“The Joys of Scheme, Cont'd”中,我们发现这些“记忆流”(在那里被称为)更简单的实现,使得tail函数本身将强制流存储在尾部哨兵中,如

    (define (tail s)
      (if (or (pair? (cdr s))
              (null? (cdr s)))
        (cdr s)
        (let ((n ((cdr s))))
           (set-cdr! s n)
           (cdr s))))
    
    ;; can be used as e.g. (https://ideone.com/v6pzDt)
    (define fibs 
       (let next-fib ((a 0) (b 1))
          (s-cons a (next-fib b (+ a b)))))
          
    

    【讨论】:

      【解决方案4】:

      这是一个尾递归形式 -

      (define (listupto n)
        (let run
          ((m 0)
           (return identity))
          (if (> m n)
              (return null)
              (run (add1 m)
                   (lambda (r) (return (cons m r)))))))
      
      (listupto 9)
      ; '(0 1 2 3 4 5 6 7 8 9)
      

      这里是流-

      (define (listupto n)
        (let run
          ((m 0))
          (if (> m n)
              empty-stream
              (stream-cons m
                           (run (add1 m))))))
      
      (stream->list (listupto 9))
      ; '(0 1 2 3 4 5 6 7 8 9)
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-09-14
        • 1970-01-01
        • 2016-01-06
        • 2020-09-16
        • 2011-07-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多