首先,当你改变常量数据时,行为是不确定的:当你引用一些东西(这里是列表)时,Lisp 环境有权将它视为一个常量。另请参阅this question,了解为什么defparameter 或defvar 优于setq。所以……
(setq list '(a b c))
(rplacd (cddr list) list)
... 最好写成:
(defparameter *list* (copy-list '(a b c)))
(setf (cdr (last *list*)) *list*)
其次,您的代码格式错误且命名约定错误(请使用破折号分隔单词);这是一个传统的布局,在 emacs 的帮助下:
(defun circularp (list)
(let (first (car list)))
(labels ((circ2 (list)
(cond
((atom list) nil)
((eq (car list) first) t)
(t (circ2 (cdr list))))))))
使用这种格式,两件事应该很明显:
let 不包含正文形式:您定义局部变量并且从不使用它们;你也可以删除let 这一行。
此外,let 缺少一对括号:您所写的定义了一个变量名first 和另一个名为car 的变量名,绑定到list。我想您想将first 定义为(car list)。
您定义了一个本地 circ2 函数,但从不使用它。我希望circularp 函数(-p 用于“谓词”,如numberp、stringp)调用(circ2 (cdr list))。我更喜欢将circ2 重命名为visit(或recurse),因为它意味着什么。
通过上述更正,那将是:
(defun circularp (list)
(let ((first (car list)))
(labels ((visit (list)
(cond
((atom list) nil)
((eq (car list) first) t)
(t (visit (cdr list))))))
(visit (cdr list)))))
但是,如果您的列表不是循环的,而是多次包含相同的元素(如'(a a b)),您会将其报告为循环,因为您检查的是它保存的数据而不是仅结构. 不要在这里查看CAR:
(defun circularp (list)
(let ((first list))
(labels ((visit (list)
(cond
((atom list) nil)
((eq list first) t)
(t (visit (cdr list))))))
(visit (cdr list)))))
此外,内部函数 是 尾递归的,但不能保证 Common Lisp 实现会自动消除尾调用(您应该检查您的实现;大多数可以根据要求进行)。这意味着您可能会分配与列表中的元素一样多的调用堆栈帧,这很糟糕。最好直接使用循环:
(defun circularp (list)
(loop
for cursor on (cdr list)
while (consp cursor)
thereis (eq cursor list)))
最后但同样重要的是:您的方法是一种非常常见的方法,但当列表不是一个大的循环单元链时失败,而只是在某处包含一个循环。例如:
CL-USER> *list*
#1=(A B C . #1#)
CL-USER> (push 10 *list*)
(10 . #1=(A B C . #1#))
CL-USER> (push 20 *list*)
(20 10 . #1=(A B C . #1#))
(参见that answer,我在这里解释了#1= 和#1# 的含义)
前面有数字的列表表现出循环性,但您不能只使用第一个 cons 单元格作为标记,因为您将在循环的子列表中永远循环。这就是 Tortoise and Hare 算法解决的问题(可能还有其他技术,最常见的是将访问过的元素存储在哈希表中)。
在您最后一次编辑之后,如果我想以递归方式检查循环性,没有labels,我会这样做:
(defun circularp (list &optional seen)
(and (consp list)
(or (if (member list seen) t nil)
(circularp (cdr list) (cons list seen)))))
我们在 seen 中跟踪所有访问过的 cons 单元格,这是可选的并初始化为 NIL(您可以传递另一个值,但这可以看作是一项功能)。
然后,如果列表是一个 cons 单元格,并且满足以下任一条件:(i) 已经存在于 seen 中,或者 (ii) 使得它的 CDR 为关于(cons list seen)的通知。
这里唯一的附加技巧是确保结果是布尔值,而不是 member 的返回值(这是要搜索的元素是第一个元素的子列表):如果您的环境有 *PRINT-CIRCLE*设置为 NIL 并且列表实际上是循环的,您不希望它尝试打印结果。
除了(if (member list seen) t nil),您还可以使用:
(when (member list seen))
(position list seen)
- 当然还有
(not (not (member list seen)))