如何仅使用cons将值按顺序附加到现有列表中



在lisp中,我试图仅使用以下基本函数将值附加到列表中:setq-cons-car和cdr。

我可以反向创建列表,但我很难弄清楚如何按顺序推送它们,如何做到这一点?

(setq list NIL)
(setq list (cons 1 list))
(setq list (cons 2 list))
(setq list (cons 3 list))
Result:
(3 2 1)

Lisp列表是单个链表。实际上,没有这样的"列表",只有一堆cons单元格,也称为对,我们按照惯例将其解释为列表(cons x y)生成一对(x.y)。作为一种特殊情况,当一个cons的cdr是一个列表时,例如,在(x.(1 2 3))中,我们将该对写为[x 1 2 3]。符号nil是空列表,因此(x.nil)就是。所以,使用cons,你所能做的就是创建一对新的。如果您有一个现有的列表l=(a b c…)和一个新元素e,您可以创建已经完成的列表(e a b c…

要修改列表的尾部,您需要能够更新现有cons单元格的cdr。例如,如果您有(a b c),在非简写形式中,它是

(setf (cdr (cdr (cdr the-list))) (list 'd))

并且您将(d)替换nil,以获得。这需要使用setf,而不是setq。因为setq只能修改变量,而不能修改任意的位置(如cons单元格的cdr)。

习惯用法是,如果你以与所需相反的顺序获得项目,则常见的习惯用法是反向创建列表,就像你正在做的那样(或使用推送等),然后在最后反向。例如,以下是地图车的单列表版本:

(defun mapkar (function list)
  "A simple variant of MAPCAR that only takes one LIST argument."
  ;; On each iteration, pop an element off of LIST, call FUNCTION with
  ;; it, and PUSH the result onto the RESULTS list.  Then,
  ;; destructively reverse the RESULTS list (it's OK to destructively
  ;; modify it, since the structure was created here), and return it.
  (let ((results '()))
    (dolist (x list (nreverse results))
      (push (funcall function x) results)))

下面是相同东西的尾部递归版本:

(defun mapkar (function list &optional (result '()))
  (if (endp list)
      (nreverse result)
      (mapkar function
              (rest list)
              (cons (funcall function (first list))
                    result))))

有一种技术(我不记得它是否有特定的名称),它包括创建一个具有任何car值的初始cons单元格,并保留一个指向cdr的指针。然后追加包括setf对指针的cdr进行CCD_3并使指针前进。您想要的列表在任何时候都将是初始cons单元格的cdr

? (progn (setq lst (cons 'head nil)) (setq lst-p lst) (cdr lst))
NIL
? (progn (setf (cdr lst-p) (cons 1 nil)) (setq lst-p (cdr lst-p)) (cdr lst))
(1)
? (progn (setf (cdr lst-p) (cons 2 nil)) (setq lst-p (cdr lst-p)) (cdr lst))
(1 2)
? (progn (setf (cdr lst-p) (cons 3 nil)) (setq lst-p (cdr lst-p)) (cdr lst))
(1 2 3)

因此,这只使用setqconscdr,也使用setf

这是一个令人惊讶的广泛主题。首先,通常情况下,您不会将单个项目附加到列表中。处理这种情况的常用方法是首先将所需的值收集到一个列表中,然后反转该列表以获得正确的顺序:

(let ((my-list '()))
  (dotimes (i 10)
    (setf my-list (cons i my-list)))
  (nreverse my-list)) ; NOTE: nreverse destructively modifies its argument
;;=> (0 1 2 3 4 5 6 7 8 9)

当然,您可以为自己编写一个函数,将值"附加"到列表中,并返回一个新值:

(defun my-append (value list)
  (if (null list)
      (cons value '()) ; or use (list value)
      (cons (first list) (my-append value (rest list)))))

您可以将其转换为尾部递归变体,但这并不能解决此函数的主要问题:它需要遍历每个元素的整个列表才能追加它。因此,这是在O(n)中,n是列表的长度。

另一个问题是,这个函数包含了很多,也就是说,它生成了很多额外的cons单元格,这给内存管理带来了很多工作。不过,人们可以通过破坏性地修改其论点来解决这个问题。

另请参阅已经是Common Lisp的一部分的函数append

为了给你一个概述,你可以选择

  • 按"错误"顺序收集值(使用cons),然后破坏性地反转该列表(nreverse
  • 按"错误"的顺序收集值,然后创建一个新的、颠倒的列表,其中的值按正确的顺序排列(reverse
  • 实际上,append将您的值添加到列表中,容忍可能的二次或更差的运行时间
  • 实际上,将您的值附加到列表中,破坏性地修改列表结构(nconc)。这是对append的改进,因为您不需要分配大量新的cons单元格,但仍然存在运行时问题

基于uselpa的"hack",我编写了使用这样的"list"操作的函数。它由一个cons组成,其car指向列表(即列表)的开头,其CCD20指向列表的最后cons单元格。尽管最后一个单元格总是(nil . nil),但简化了附加值:

首先,将最后一个单元格的car设置为新值,然后将cdr设置为新单元格(nil . nil),最后返回新的"列表":

(defun empty-al ()
  (let ((cell (cons nil nil)))
    (cons cell cell)))
(defun cons-al (value al)
  (cons (cons value (car al))
        (cdr al)))
(defun append-al (value al)
  (let ((new-cell (cons nil nil)))
    (setf (cadr al) value
          (cddr al) new-cell)
    (cons (car al) new-cell)))

通常的习惯用法是使用扩展的loop:collect特性,或者通过推(如您所述)进行累积并在最后反转。

举个例子,这里有两个版本的简单range函数,它返回n以下0的整数:

(defun range (n)
  (loop :for i :below n
        :collect i))
(defun range (n)
  (let ((list ()))
    (dotimes (i n)
      (push i list))
    (reverse list)))

在您提到的限制下,我建议您实现自己的reverse并使用它。这里有一个尾部递归示例,但请注意,Lisp实现不需要支持尾部递归。

(defun own-reverse (list &optional (acc ()))
  (if (endp list)
      acc
      (own-reverse (rest list)
                   (cons (first list) acc))))

相关内容

  • 没有找到相关文章

最新更新