【问题标题】:How to reverse list in Scheme?如何在Scheme中反转列表?
【发布时间】:2016-01-11 07:49:43
【问题描述】:
(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)))))

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

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

【问题讨论】:

  • 显示测试用例和预期输出。

标签: algorithm scheme


【解决方案1】:

很少使用列表的“后端”来处理任何事情,它既低效又容易导致相当复杂的代码(如您所见)。

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

也就是说,

(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,而不需要重复遍历结果在其后面添加东西(这是append所做的) .

【讨论】:

  • 太棒了!非常感谢!
【解决方案2】:

如果您使用通常的carcdr 过程处理列表,则从前到后处理它。使用cons 构造一个列表会从从后到前构造它。所以你可以结合这两种行为来反转一个列表;只需查看列表并将cons car 放入累加器:

(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.18 SICP 练习(反向列表)并且作者没有引入循环语句跨度>
  • 可选参数不是标准的。例如。来自 DrRacket 中 #!r6rs 的错误消息:#%plain-lambda: not an identifier in: (acc null)
  • @Sylwester OP 在他的代码中使用null,所以它不是 R6RS。
【解决方案3】:

定义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)

【讨论】:

  • @ivan_petrushenko 我已经扩展了答案。我希望这会有所帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-30
  • 2011-04-25
  • 2020-02-18
  • 2021-07-28
相关资源
最近更新 更多