如何在Scheme中编写Push和Pop?

时间:2009-06-25 00:20:26

标签: lisp scheme racket

现在我有

(define (push x a-list)
  (set! a-list (cons a-list x)))

(define (pop a-list)
  (let ((result (first a-list)))
    (set! a-list  (rest a-list))
    result))

但我得到了这个结果:

Welcome to DrScheme, version 4.2 [3m].
Language: Module; memory limit: 256 megabytes.
> (define my-list (list 1 2 3))
> (push 4 my-list)
> my-list
(1 2 3)
> (pop my-list)
1
> my-list
(1 2 3)

我做错了什么?是否有更好的方法来编写push,以便在结尾处添加元素并弹出以便元素从第一个元素中删除?

6 个答案:

答案 0 :(得分:7)

这是关于在代码中使用变异的一点:没有必要为此跳转到宏。我现在假设堆栈操作:为了得到一个可以传递和变异的简单值,你需要的只是列表中的一个包装器,其余的代码保持不变(好吧,这是一个微小的改变,使得它可以正确堆叠操作)。在PLT Scheme中,这正是盒子的用途:

(define (push x a-list)
  (set-box! a-list (cons x (unbox a-list))))

(define (pop a-list)
  (let ((result (first (unbox a-list))))
    (set-box! a-list (rest (unbox a-list)))
    result))

另请注意,您可以使用begin0代替let

(define (pop a-list)
  (begin0 (first (unbox a-list))
    (set-box! a-list (rest (unbox a-list)))))

至于将其转换为队列,您可以使用上述方法之一,但除了Jonas编写的最后一个版本之外,解决方案效率非常低。例如,如果您按照Sev的建议行事:

(set-box! queue (append (unbox queue) (list x)))

然后复制整个队列 - 这意味着向队列中添加项目的循环会在每次添加时复制所有内容,从而为GC生成大量垃圾(考虑附加一个字符到循环中字符串的结尾)。 “未知(谷歌)”解决方案修改了列表并在其末尾添加了一个指针,因此它避免了生成垃圾来收集,但它仍然效率低下。

Jonas编写的解决方案是执行此操作的常用方法 - 保持指向列表末尾的指针。但是,如果您想在PLT Scheme中执行此操作,则需要使用可变对:mconsmcarmcdrset-mcar!set-mcdr!。 PLT中通常的对是不可变的,因为版本4.0出来了。

答案 1 :(得分:5)

  1. 您只是设置绑定到词法变量a-list的内容。函数退出后,该变量不再存在。

  2. cons制作一个新的cons小区。 cons单元由两部分组成,称为carcdr。列表是一系列缺点单元格,其中每个汽车保持一些值,每个cdr指向相应的下一个单元格,最后一个cdr指向nil。当您编写(cons a-list x)时,会创建一个新的cons单元格,其中包含对汽车中a-list的引用,以及cdr中的x,这很可能不是您想要的。

  3. pushpop通常被理解为对称操作。当您将某些内容推送到列表(作为堆栈)时,您希望在之后直接弹出此列表时将其恢复。由于列表始终在其开头引用,因此您希望通过执行(cons x a-list)来推送列表。

  4. IANAS(我不是Schemer),但我认为获得所需内容的最简单方法是使push成为扩展为{{define-syntax的宏(使用(set! <lst> (cons <obj> <lst>))) 1}}。否则,您需要将引用传递到列表中的push函数。类似于pop。传递引用可以通过包装到另一个列表中来完成。

答案 2 :(得分:3)

Svante是正确的,使用宏是惯用法。

这是一种没有宏的方法,但是在下方你不能使用普通列表作为队列。 至少与R5RS一起使用,在导入正确的库后应该在R6RS中工作。

(define (push x queue)
  (let loop ((l (car queue)))
    (if (null? (cdr l))
      (set-cdr! l (list x))
      (loop (cdr l)))))

 (define (pop queue)
   (let ((tmp (car (car queue))))
     (set-car! queue (cdr (car queue)))
     tmp))

(define make-queue (lambda args (list args)))

(define q (make-queue 1 2 3))

(push 4 q)
q
; ((1 2 3 4))
(pop a)
; ((2 3 4))
q

答案 3 :(得分:1)

我想您正在尝试实施queue。这可以通过多种方式完成,但如果您希望插入和删除操作都在恒定时间内执行,O(1),则必须保持对前面和后面的引用。队列。

您可以将这些引用保存在cons cell或我的示例中,包含在闭包中。

术语pushpop通常在处理堆栈时使用,因此我在下面的代码中将这些更改为enqueuedequeue

(define (make-queue)
  (let ((front '())
        (back '()))
    (lambda (msg . obj)
      (cond ((eq? msg 'empty?) (null? front))
            ((eq? msg 'enqueue!) 
             (if (null? front)
                 (begin 
                   (set! front obj)
                   (set! back obj))
                 (begin
                   (set-cdr! back obj)
                   (set! back obj))))
            ((eq? msg 'dequeue!) 
             (begin
               (let ((val (car front)))
                 (set! front (cdr front))
                 val)))
            ((eq? msg 'queue->list) front)))))

make-queue返回一个过程,该过程将变量frontback中的队列状态包装起来。此过程接受将执行队列数据结构过程的不同消息。

此程序可以这样使用:

> (define q (make-queue))
> (q 'empty?)
#t
> (q 'enqueue! 4)
> (q 'empty?)
#f
> (q 'enqueue! 9)
> (q 'queue->list)
(4 9)
> (q 'dequeue!)
4
> (q 'queue->list)
(9)

这几乎是Scheme中的面向对象编程!您可以将frontback视为队列类的私有成员,将消息视为方法。

调用约定有点落后,但很容易将队列包装在更好的API中:

(define (enqueue! queue x)
   (queue 'enqueue! x))

(define (dequeue! queue)
  (queue 'dequeue!))

(define (empty-queue? queue)
  (queue 'empty?))

(define (queue->list queue)
  (queue 'queue->list))

修改

正如Eli指出的那样,PLT Scheme默认情况下对immutable,这意味着没有set-car!set-cdr!。要使代码在PLT Scheme中工作,您必须使用mutable pairs。在标准方案(R4RS,R5RS或R6RS)中,代码应该不加修改。

答案 4 :(得分:0)

您正在做的是仅在本地修改“队列”,因此结果在定义的范围之外不可用。这是因为,在方案中,一切都是通过值传递的,而不是通过引用传递的。而Scheme结构是不可变的。

(define queue '()) ;; globally set

(define (push item)
  (set! queue (append queue (list item))))

(define (pop)
  (if (null? queue)
      '()
      (let ((pop (car queue)))
        (set! queue (cdr queue))
        pop)))

;; some testing
(push 1)
queue
(push 2)
queue
(push 3)
queue
(pop)
queue
(pop)
queue
(pop)

问题依赖于以下问题:在Scheme中,数据和操作遵循 无副作用规则

因此,对于真正的队列,我们​​需要可变性,这是我们没有的。所以我们必须设法绕过它。

由于方案中的所有内容都通过值传递,而不是通过引用,因此事物保持在本地并保持不变, 没有副作用。 因此,我选择创建一个全局队列,通过全局应用我们对结构的更改来避免这种情况,而不是传递任何内容。

在任何情况下,如果你只需要1个队列,这个方法可以正常工作,虽然它是内存密集型的,因为你每次修改结构时都要创建一个新对象。

为了获得更好的结果,我们可以使用宏来自动创建队列。

答案 5 :(得分:0)

在列表上运行的push和pop宏可以在许多Lispy语言中找到:Emacs Lisp,Gauche Scheme,Common Lisp,Chicken Scheme(在miscmacros egg中),Arc等。

Welcome to Racket v6.1.1.
> (define-syntax pop!
  (syntax-rules ()
    [(pop! xs)
     (begin0 (car xs) (set! xs (cdr xs)))]))
> (define-syntax push!
  (syntax-rules ()
    [(push! item xs)
     (set! xs (cons item xs))]))
> (define xs '(3 4 5 6))
> (define ys xs)
> (pop! xs)
3
> (pop! xs)
4
> (push! 9000 xs)
> xs
'(9000 5 6)
> ys  ;; Note that this is unchanged.
'(3 4 5 6)

请注意,即使列表在Racket中不可变,这仍然有效。一个项目是&#34;弹出&#34;从列表中只需调整指针即可。