在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)
因此,这只使用setq
、cons
、cdr
,也使用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))))