如何在Scheme中反转列表?

时间:2016-01-11 07:49:44

标签: algorithm scheme

(define (list-without-last-pair items)
  (let ((s (cdr items)))
    (if (null? s)
    null
    (cons (car items)
          (list-without-last-pair s)))))

(define (only-last-pair items)
  (let ((s (cdr items)))
    (if (null? s)
        (car items)
        (only-last-pair s))))

(define (reverse items)
  (if (null? items)
       null
       (cons (only-last-pair items)
             (reverse (list-without-last-pair items)))))

我的main方法和辅助方法中有很多代码重复。如何避免这种情况并改进解决方案?

预期输出:(reverse (list 1 2 3)) => (3 2 1)

3 个答案:

答案 0 :(得分:7)

将列表的“后端”用于任何事情是非常罕见的,它既低效又容易导致相当复杂的代码(正如您所注意到的那样)。

为了反转列表,您可以保存第一个元素,反转其余部分,然后将旧的第一个元素放在“反向休息”的后面。
(这与您正在进行的操作相同,但在列表的另一端。)

即,

(define (reverse lst)
    (if (null? lst)
        lst
        (append (reverse (cdr lst)) (list (car lst)))))

这是非常低效的,所以通常你会使用尾递归版本(SICP中的“迭代过程”)。

如果将它分解为主函数和“帮助器”,则最容易理解尾递归实现:

(define (reverse-helper lst acc)
  (if (null? lst)
      acc
      (reverse-helper (cdr lst) (cons (car lst) acc))))

(define (reverse lst)
  (reverse-helper lst '()))

主要区别在于,在acc参数中构建结果意味着我们可以使用cons而不需要重复遍历结果以在其后面添加内容(这就是{ {1}}确实如此。

答案 1 :(得分:3)

如果您使用通常的carcdr程序处理列表,则可以从正面到背面处理它。使用cons构建列表会将其从构造回到前面。因此,您可以将这两种行为结合起来以反转列表;只需查看列表并conscar添加到累加器:

(define (reverse lst)
  (let loop ((lst lst) (acc null))
    (if (null? lst)
        acc
        (loop (cdr lst) (cons (car lst) acc)))))

请注意,loop 不是预定义的过程或关键字(与Common Lisp相对),而只是我为内部过程选择的名称;上面的代码与

相同
(define (reverse lst)
  (define (loop lst acc)
    (if (null? lst)
        acc
        (loop (cdr lst) (cons (car lst) acc))))
  (loop lst null))

或者,如果您想避免使用2个过程,可以使用具有默认值的可选参数:

(define (reverse lst (acc null))
  (if (null? lst)
      acc
      (reverse (cdr lst) (cons (car lst) acc))))

答案 2 :(得分:0)

定义reverse就像使用cons

将现有列表折叠到新的空列表一样简单
(define (reverse xs)
  (foldl cons '() xs))

要了解它的工作原理,请评估折叠

(reverse '(1 2 3)) ;; ⇒ ?

;; first iteration
(cons 1 '()) ;; ⇒ '(1)

;; second iteration
(cons 2 '(1)) ;; ⇒ '(2 1)

;; third iteration
(cons 3 '(2 1)) ;; ⇒ '(3 2 1)

在评论中,您询问了如何实施foldl

(define (foldl f y xs)
  (if (empty? xs)
      y
      (foldl f
             (f (car xs) y)
             (cdr xs))))

如果您不熟悉折叠,我认为使用sum函数演示它们是最简单的。

如果你想总结一个数字列表1 2 3 4,你会怎么做?可能类似

1 + 2 + 3 + 4

你看到每个之间放置+吗?让我们看看我们如何评估这个

((1 + 2) + 3) + 4
(3 + 3) + 4
6 + 4
⇒ 10

foldl就是这样做的。它需要二进制过程,初始值和列表。在我们的案例中,我们将使用过程+0的初始值进行演示。这次我们将使用s表达式((+ x y)而不是中缀x + y)来显示评估。

(foldl + 0 '(1 2 3 4))
(+ 4 (+ 3 (+ 2 (+ 1 0))))
(+ 4 (+ 3 (+ 2 1)))
(+ 4 (+ 3 3))
(+ 4 6)
⇒ 10

这个初始值很重要,因为如果输入是一个空列表,我们需要知道期待什么样的值

(foldl + 0 '())
;; ⇒ 0

所以,让我们用折叠来定义sum

(define (sum xs) (foldl + 0 xs))
(sum '(1 2 3 4)) ;; ⇒ 10
(sum '())        ;; ⇒ 0

总和很容易让我们理解,因为它们对我们来说非常熟悉,但reverse程序可能不那么清楚。折叠减少到单个值,在我们的例子中,我们将输入列表减少到单个输出列表。

让我们快速重新审视sum评估。请记住,我们折叠的程序为+,初始值为0

(foldl + 0 '(1 2 3 4))
(+ 4 (+ 3 (+ 2 (+ 1 0))))
(+ 4 (+ 3 (+ 2 1)))
(+ 4 (+ 3 3))
(+ 4 6)
⇒ 10

现在让我们看看写出的reverse评估。这里我们折叠的过程是cons,初始值是'()(空列表)

(foldl cons '() '(1 2 3))
(cons 3 (cons 2 (cons 1 '())))
(cons 3 (cons 2 '(1)))
(cons 3 '(2 1))
⇒ '(3 2 1)