球拍/方案展平解释

时间:2012-11-25 03:19:05

标签: recursion scheme racket flatten

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

版本1,是从球拍本身,而第二版是更常见的?实施

(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)之前打印,这似乎是第一个在append内部展平的调用首先被评估。

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

非常感谢。

2 个答案:

答案 0 :(得分:5)

不是你问题的答案(克里斯已经提供了一个很好的答案!),但为了完整起见,这是实现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有更多共同点)的方法:

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

答案 1 :(得分:4)

主要区别在于:

  • flatten1通过将输出元素(首先从cdr侧,然后从car侧)存储到累加器中来工作。这是有效的,因为列表是从右到左构建的,因此首先在cdr方面进行操作是正确的。
  • flatten2通过递归展平carcdr边,然后append将它们放在一起来工作。

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

总而言之,我会考虑car初学者的flatten实现,flatten2更精致,专业版。另请参阅my implementation of flatten,其使用与flatten1相同的原则,但使用左侧折叠而不是flatten1使用的右侧折叠。

(左侧解决方案使用较少的堆栈空间,但可能使用更多的堆空间。右侧折叠解决方案使用更多堆栈,通常更少堆,但快速读取flatten1表明在这种情况下堆使用与我的实现大致相同。)