Skip to content

LISP - 列表修改

在 Common Lisp 中,列表是典型的(或核心的)数据结构。修改列表是一个常见的任务,Lisp 为此提供了两类不同的函数:返回新的、已修改列表的**非破坏性(non-destructive)函数,以及直接修改原始列表内存结构的破坏性(destructive)**函数。

理解这种区别是熟练 Lisp 编程最关键的方面之一,因为它直接影响程序的正确性和性能。

操作类型描述关键函数
非破坏性(函数式风格)这些函数创建并返回一个包含所需更改的新列表,而不会触及原始列表。这更安全,也更容易理解,尤其是在复杂的程序中。append, remove
破坏性(命令式风格)这些函数就地修改原始列表的 cons 单元。这可能更快,内存效率更高,因为它避免了创建新对象。按照惯例,它们的名称通常包含 ‘n’ 或 ‘r’。nconc, delete, rplaca, rplacd, push, pop

让我们比较 append(非破坏性)和 nconc(破坏性)。

(defparameter *list-a* '(a b c))
(defparameter *list-b* '(d e f))
;; 1. 非破坏性 APPEND
(let ((new-list (append *list-a* *list-b*)))
(format t "Result of APPEND: ~a~%" new-list)
(format t "Original *list-a* is unchanged: ~a~%~%" *list-a*))
;; 2. 破坏性 NCONC
(let ((modified-list (nconc *list-a* *list-b*)))
(format t "Result of NCONC: ~a~%" modified-list)
(format t "Original *list-a* is NOW MODIFIED: ~a~%" *list-a*))
Result of APPEND: (A B C D E F)
Original *list-a* is unchanged: (A B C)
Result of NCONC: (A B C D E F)
Original *list-a* is NOW MODIFIED: (A B C D E F)

警告:使用 nconc 会修改 *list-a* 的最后一个 cons 单元,使其指向 *list-b*。这是一个永久性更改。

类似地,remove(非破坏性)与 delete(破坏性)形成对比。

;; 1. 非破坏性 REMOVE
(let ((original '(a b c b d)))
(let ((new-list (remove 'b original)))
(format t "Result of REMOVE: ~a~%" new-list)
(format t "Original is unchanged: ~a~%~%" original)))
;; 2. 破坏性 DELETE
(let ((original (list 'a 'b 'c 'b 'd)))
;; 我们必须使用 (list ...) 来创建一个可以安全修改的新列表。
(let ((modified-list (delete 'b original)))
(format t "Result of DELETE: ~a~%" modified-list)
(format t "Original is NOW MODIFIED: ~a~%" original)))
Result of REMOVE: (A C D)
Original is unchanged: (A B C B D)
Result of DELETE: (A C D)
Original is NOW MODIFIED: (A C D)

最佳实践:只在你刚创建且确定未在程序其他地方共享的列表上使用 delete。

push 和 pop 是高效的破坏性宏,用于将列表作为栈来处理。

(defparameter *stack* '(a b c))
(format t "Initial stack: ~a~%" *stack*)
;; 将一个元素 PUSH 到列表头部
(push 'x *stack*)
(format t "After PUSH 'x: ~a~%" *stack*)
;; 从列表头部 POP 一个元素
(let ((removed-item (pop *stack*)))
(format t "Item popped: ~a~%" removed-item)
(format t "After POP: ~a~%" *stack*))
Initial stack: (A B C)
After PUSH 'x: (X A B C)
Item popped: X
After POP: (A B C)

rplaca(替换 car)和 rplacd(替换 cdr)是低级的破坏性运算符,它们直接改变 cons 单元的指针。使用它们时务必极其谨慎。

(let ((my-list (list 'a 'b 'c)))
(format t "Original: ~a~%" my-list)
;; 替换第一个元素(第一个 cons 单元的 CAR 部分)
(rplaca my-list 'x)
(format t "After (rplaca my-list 'x): ~a~%" my-list)
;; 替换列表的其余部分(第一个 cons 单元的 CDR 部分)
(rplacd my-list '(y z))
(format t "After (rplacd my-list '(y z)): ~a~%" my-list))
Original: (A B C)
After (rplaca my-list 'x): (X B C)
After (rplacd my-list '(y z)): (X Y Z)