【问题标题】:Racket/Scheme Flatten Explanations球拍/方案展平解释
【发布时间】:2012-11-12 22:23:49
【问题描述】:

有人可以帮我准确分解以下 flatten 版本的执行顺序吗?我正在使用球拍。

版本 1,是来自球拍本身,而版本 2 是更常见的?实施。

(define (flatten1 list)
  (let loop ([l list] [acc null])
    (printf "l = ~a acc = ~a\n" l acc)
    (cond [(null? l) acc]
          [(pair? l) (loop (car l) (loop (cdr l) acc))]
          [else (cons l acc)])))

(define (flatten2 l)
  (printf "l = ~a\n" l)
  (cond [(null? l) null]
        [(atom? l) (list l)]
        [else (append (flatten2 (car l)) (flatten2 (cdr l)))]))

现在,使用 '(1 2 3) 运行第一个示例会产生:

l = (1 2 3) acc = ()
l = (2 3) acc = ()
l = (3) acc = ()
l = () acc = ()
l = 3 acc = ()
l = 2 acc = (3)
l = 1 acc = (2 3)
'(1 2 3)

而第二个产生:

l = (1 2 3)
l = 1
l = (2 3)
l = 2
l = (3)
l = 3
l = ()
'(1 2 3)

执行顺序似乎不同。在第一个示例中,看起来第二个循环 (loop (cdr l) acc) 在第一个循环之前触发,因为 '(2 3) 正在立即打印。而在第二个示例中,1 在 '(2 3) 之前打印,这似乎是先评估第一个调用 flatten 的 append 内部。

我正在浏览 Little Schemer,但这些是更难的示例,我真的可以使用一些帮助。

非常感谢。

【问题讨论】:

    标签: recursion scheme racket flatten


    【解决方案1】:

    主要区别在于:

    • flatten1 通过将输出元素(首先从cdr 端,然后从car 端)存储到累加器中来工作。这是可行的,因为列表是从右到左构建的,所以首先在 cdr 一侧工作是正确的。
    • flatten2 的工作原理是递归地压平 carcdr 边,然后将 append 合并在一起。

    flatten1 更快,特别是如果树在car 一侧很重:使用累加器意味着无论如何都没有额外的列表复制。而flatten2 中的append 调用会导致append 的左侧被复制,这意味着如果car 一侧的树很重,则需要进行大量额外的列表复制。

    所以总而言之,我认为flatten2 是初学者的扁平化实现,flatten1 是更精致、更专业的版本。另请参阅 my implementation of flatten,其工作原理与 flatten1 相同,但使用左折叠而不是 flatten1 使用的右折叠。

    (左折叠解决方案使用更少的堆栈空间,但可能使用更多的堆空间。右折叠解决方案使用更多的堆栈,通常使用更少的堆,尽管快速阅读flatten1 表明在这种情况下堆使用量约为和我的实现一样。)

    【讨论】:

    • 谢谢。但是为什么在示例一中 (loop (cdr l)) 在 (loop (car l)) 之前触发,而在示例二中 (flatten2 (car l)) 在 (flatten2 (cdr l)) 之前触发?
    • 因此,在flatten1 中,cdr 排在car 之前,原因是我描述的:累加器列表是从右到左构建的。在flatten2 中,顺序无关紧要,但Racket 在评估表达式时总是使用从左到右的顺序,所以这就是为什么你在cdr 之前看到car
    • 好的,谢谢。所以在 Racket 中,列表(cons)是从右到左构建的,但表达式是从左到右计算的。这有帮助。
    • 在所有的 Scheme 系统中,列表总是从右到左构建的,但是子表达式的求值顺序是未指定的(这意味着不同的实现可以使用任何他们喜欢的顺序)。 Racket 开发人员有意识地决定始终使用从左到右的评估;其他 Scheme 实现可能表现出不同的(不一定是确定性的)顺序。
    • 最初,我认为 flatten1 中的循环调用也应该从左到右进行评估,但我困惑的主要原因是第二次调用循环是第一次调用的第二个参数循环,因此第二个循环首先评估,而对 flatten2 的第二次调用是 append 的独立参数,因此在第一个参数被评估后触发。
    【解决方案2】:

    并不是你的问题的真正答案(克里斯已经提供了一个很好的答案!),但为了完整起见,这里还有另一种实现 flatten 的方法,类似于 flatten2,但更简洁一点:

    (define (atom? x)
      (and (not (null? x))
           (not (pair? x))))
    
    (define (flatten lst)
      (if (atom? lst)
          (list lst)
          (apply append (map flatten lst))))
    

    另一种实现左折叠版本的方法(与flatten1 有更多共同点),使用标准 Racket 程序:

    (define (flatten lst)
      (define (loop lst acc)
        (if (atom? lst)
            (cons lst acc)
            (foldl loop acc lst)))
      (reverse (loop lst '())))
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-09-28
      • 1970-01-01
      • 1970-01-01
      • 2022-07-27
      • 2016-08-07
      • 2019-10-27
      • 1970-01-01
      相关资源
      最近更新 更多