【问题标题】:Scheme: Iterative process to reconstruct a list in original order?方案:以原始顺序重建列表的迭代过程?
【发布时间】:2020-12-28 19:33:05
【问题描述】:

我的问题是:如何编写一个利用尾调用的过程,并且构造一个不按相反顺序的列表。 为了说明我的意思,下面是一个非常简单的迭代过程示例,它创建了一个列表的副本:

(define (copy-list ls)
    (define (iter cp-ls rest-ls)
      (if (null? rest-ls)
          cp-ls
          (iter (cons (car rest-ls) cp-ls) (cdr rest-ls))))
    (iter '() ls))

问题在于,由于元素consed 在一起的迭代顺序,返回的列表最终被颠倒了。是的,您可以通过在if 块中执行(reverse cp-list) 而不仅仅是cp-list 来轻松解决此问题,但问题在于reverse 是一个递归过程。使用此过程将抵消尾调用的优势,即堆栈大小随输入大小线性增长。

那么,基本上,如何编写像上面提到的那样以正确顺序返回列表的过程不使用任何类型的递归过程?

谢谢

【问题讨论】:

  • (iter '() ls) 更改为(iter '() (iter '() ls)) 是否可行?
  • @RowPJ 我不在乎别人怎么说,但这实际上是一种天才。谢谢
  • 我不认为reverse 是作为递归过程实现的,实际上我认为它的实现与您的iter 过程几乎完全一样。所以你的程序基本上是在做(reverse (reverse ls))。如果你不改变列表,实际上没有必要“复制”它们,它在你的程序中没有任何有用的效果。
  • tailcall 的优点是堆栈大小不会随着输入大小线性或其他方式增长,而是保持不变,就像你的 iter 一样只是在它的两个参数中处理数据。 ---- 在 Racket 中,堆栈位于堆上,因此迭代和 线性 递归过程之间没有太大区别。 ---- 在 r5rs 方案中,您可以使用 set-cdr!-ing 和 head sentinel,如所见,例如hereelsewhere
  • @WillNess 是的,tailcall 意味着 恒定 堆栈增长。我实际上知道这一点,而我写的确实是脑放屁。感谢您的澄清。一个问题:无论堆栈在哪里,迭代和线性过程的增长率仍然存在?那么,当你说 Racket 中的迭代和递归没有区别时,这究竟是什么意思呢?我的意思是,在递归过程中,每次对内存的递归调用都是解析最终返回值所必需的,而迭代过程并非如此。

标签: list functional-programming scheme lisp tail-call-optimization


【解决方案1】:

请注意,您的 iter 过程几乎完全首先是如何实现 reverse - 不,这不是您在问题中提到的递归过程。

在 Racket 中检查过程的定义很简单:在没有定义的编辑窗口中键入reverse,右键单击它,然后选择“跳转到定义”。它将有一些额外的代码用于优化和错误处理,但算法的核心在letrec-values 部分,它与您的iter 过程相同。所以,如果我们已经拥有了:

(define (reverse ls)
  (let iter ((cp-ls '()) (rest-ls ls))
    (if (null? rest-ls)
        cp-ls
        (iter (cons (car rest-ls) cp-ls) (cdr rest-ls)))))

那么,copy-list 就是:

(define (copy-list ls)
  (reverse (reverse ls)))

顺便说一句,这并不是非常有用 - 如果你不改变列表,复制它是没有意义的。反向的反向只是原始的东西,不是吗?事实上,任何对不可变列表进行操作的copy-list 实现,无论出于何种意图和目的,都等效于身份过程:

(define (copy-list ls) ls)

【讨论】:

  • 嗯,我并不真正关心这个特定过程的有用性,而是在堆栈大小增长方面保持过程严格迭代的想法。我认为reverse 妥协了这一点。但如果真的如你所说;那reverse其实是一个迭代过程,那么它真的不是问题。感谢您的智慧!
  • 顺便说一句,我忘了问你一件事。你如何确定reverse 的实现?无论如何,看看 Racket 是如何实现这个过程的(r5rs)?
  • Racket 中的@Snusifer 很简单:在没有定义的窗口中输入reverse,右键单击它,然后选择“跳转到定义”。它会有一些额外的错误处理代码,但算法的核心在letrec-values 部分,它与您的iter 过程相同。
【解决方案2】:

您可以在下面使用延续传递样式,return -

(define (copy-list ls)
  (let loop ((ls ls) (return identity))
    (if (null? ls)
        (return null)
        (loop (cdr ls)
              (lambda (r) (return (cons (car ls) r)))))))

(copy-list '(1 2 3 4))
; '(1 2 3 4)

这是流程的样子 -

(copy-list '(1 2 3 4))
(loop '(1 2 3 4) identity)
(loop '(2 3 4) (lambda (r) (identity (cons 1 r))))
(loop '(3 4) (lambda (r) ((lambda (r) (identity (cons 1 r))) (cons 2 r))))
(loop '(4) (lambda (r) ((lambda (r) ((lambda (r) (identity (cons 1 r))) (cons 2 r))) (cons 3 r))))
(loop '() (lambda (r) ((lambda (r) ((lambda (r) ((lambda (r) (identity (cons 1 r))) (cons 2 r))) (cons 3 r))) (cons 4 r))))
((lambda (r) ((lambda (r) ((lambda (r) ((lambda (r) (identity (cons 1 r))) (cons 2 r))) (cons 3 r))) (cons 4 r))) null)
((lambda (r) ((lambda (r) ((lambda (r) (identity (cons 1 r))) (cons 2 r))) (cons 3 r))) '(4))
((lambda (r) ((lambda (r) (identity (cons 1 r))) (cons 2 r))) '(3 4))
((lambda (r) (identity (cons 1 r))) '(2 3 4))
(identity '(1 2 3 4))
'(1 2 3 4)

【讨论】:

  • 我认为它应该不会慢很多,用渐近术语来说都是 ~2n。 lambda 不会在内部回溯,而是从外部解开,因此它只是另一个 ~ n 遍历。即使我们说每次解开(即应用程序)都需要时间(除了 cons 本身需要时间),那么它最多是 ~3n 。 :)
  • 理论上它应该有 n-runtime,但我之前已经对其进行了几次计时,并记得它要慢得多。我想这与分配/评估 lambda 的运行时成本有关。或者也许还有其他优化,例如tail-recursion modulo cons?。我对方案实施细节不够熟悉,不知道它们在哪里“快”或“慢”。
  • 嗯,我只是在 10,000 个元素的列表上重新运行了一个基准测试,并没有太大的区别。事实上,double-reverse 稍微慢了一点!感谢分享威尔
  • 不客气。当您在这里时,也许还可以修复括号并在 lambdas 中将其设为lambda (r)?顺便说一句,您编写它的方式可能会给人一种意想不到的印象,即每个 lambdas 都被完全评估(然后它是二次的)。 here's 另一种写法,显然是线性的。 (不幸的是它不是完全有效的方案......)
【解决方案3】:
(map (lambda(x) x) l)

将复制列表l,并且它不是递归编写的。

(let ((l '(1 2 3 4)))
  ((fold-right (lambda (e acc)
                 (lambda (x) (cons x (acc e))))
               (lambda (x) (list x))
               (cdr l))
   (car l)))

是另一种不递归复制列表的形式,但使用一个幺半群。

其他:

(let ((l '(1 2 3 4)))
  (car ((fold-right (lambda (e acc)
                      (lambda (x) (acc (append x (list e)))))
                    (lambda (x) (list x))
                    (cdr l))
        (list (car l)))))

其他:

(let ((l '(1 2 3 4)))
  (cdr ((fold-left (lambda (acc e)
                     (lambda (x) (cons x (acc e))))
                   (lambda (x) (list x))
                   l)
        'first)))

其他(Will 建议):

(let ((l '(1 2 3 4)))
  ((fold-right (lambda (e acc)
                 (lambda (k) (k (acc (lambda (es) (cons e es))))))
               (lambda (z) (z (list))) l)
   (lambda (es) es)))

还有很多其他方法可以复制列表。一般来说,要制作副本,您需要直接或间接致电cons

如 cmets 中所述,并非所有这些方式都使用迭代过程。

【讨论】:

  • 这是不正确的。 fold-right 是一个 recursive 过程(与 fold-left 不同,它是一个 iterative 过程),因此您不符合问题中的要求。
  • 非常有趣,谢谢。您也可以使用第一个 fold-right sn-p 来执行 'firstcdr 技巧。我从它开始,得到了(let ((l '(1 2 3 4))) ((fold-right (lambda (e acc) (lambda (x) (x (acc (lambda (ys) (cons e ys)))))) (lambda (x) (x (list))) l) (lambda (x) x)))
  • 它使用等价的((cons-to xs) x) == ((call-with xs) {cons x}),其中{cons x} = (/\ xs . (cons x xs))((call-with xs) x) = (x xs)
  • @WillNess 我过去学过一点 Haskell,但不足以掌握 monad。你能推荐我一种学习它的方法,这样可以培养直觉吗?我想了解如何在语言内核中包含 monad...我在 scheme 中使用 monad 进行了编程,但不够深入...
  • 我可以提供一些口号,比如“Monads 是 EDSL”、“Monads 是一流的程序”、“Monads 是类型编码的具体计算”、“Monads 是 Functors,首先”、“Monads 是通用嵌套循环”,“Monads 是通用函数应用程序”等。请参阅stackoverflow.com/questions/44965/…stackoverflow.com/tags/monads/infowiki.haskell.org/Monadstackoverflow.com/q/11234632/849891,以及我的个人资料页面上的一些 monad 相关链接,或者我的一些 monad 答案。
猜你喜欢
  • 2023-04-06
  • 2019-01-15
  • 2017-02-16
  • 2017-03-08
  • 1970-01-01
  • 2019-11-12
  • 1970-01-01
  • 2012-10-25
  • 2020-05-19
相关资源
最近更新 更多