在 Common Lisp 中有多种实现方式。该示例应该足够小,以便您遵循(测试它们)。
递归实现
您的方法很好,除了您有小错误(除了 cmets 中报告的其他错误):
- 不要将
SETF 用于未声明的变量。
- 在基本情况下不要返回
NIL:您的函数应该返回一个数字。
- 另外,您的代码可以更好地格式化,并且您应该使用更长的名称(尤其是小写的
l 难以阅读)
这是修改后的版本:
(defun times (list element)
(cond
((null list) 0)
((equal (car list) element) (1+ (times (cdr list) element)))
(t (times (cdr list) element))))
示例
让我们TRACE函数:
CL-USER> (trace times)
这是执行跟踪:
CL-USER> (times '(a b c d a f a) 'a)
0: (TIMES (A B C D A F A) A)
1: (TIMES (B C D A F A) A)
2: (TIMES (C D A F A) A)
3: (TIMES (D A F A) A)
4: (TIMES (A F A) A)
5: (TIMES (F A) A)
6: (TIMES (A) A)
7: (TIMES NIL A)
7: TIMES returned 0
6: TIMES returned 1
5: TIMES returned 1
4: TIMES returned 2
3: TIMES returned 2
2: TIMES returned 2
1: TIMES returned 2
0: TIMES returned 3
3
您可以看到,对于列表中访问的每个元素,调用堆栈都会增长。这通常是一种不好的做法,尤其是当递归函数基本上是在实现循环时。
循环
使用简单的LOOP:
(defun times (list element)
(loop for value in list count (equal value element)))
或者,使用DOLIST:
(defun times (list element)
(let ((counter 0))
(dolist (value list counter)
(when (equal element value)
(incf counter)))))
这里counter是LET引入的局部变量。它在循环内以INCF 递增,只有WHEN 比较成立。最后,counter 从dolist 中返回(第三个参数表示要评估哪个表单具有结果值)。 dolist的返回值也是let和整个函数的返回值。
这也可以用DO重写:
(defun times (list element)
(do ((counter 0)) ((null list) counter)
(when (equal element (pop list))
(incf counter))))
do 中的第一个列表引入了绑定,第二个列表是终止测试(这里我们在列表为空时停止),然后是一个结果表单(这里是计数器)。在循环体内,我们POP 输入列表中的元素并像以前一样进行比较。
尾递归实现
如果您想保留递归实现,请添加一个累加器并在进入递归评估之前计算所有中间结果。如果所有结果都作为函数参数传递,则无需在递归的每一步跟踪中间结果,这甚至消除了分配堆栈帧的需要。语言规范没有明确要求执行尾调用消除的能力,但它通常在大多数实现中都可用。
(defun times (list element)
(labels ((recurse (list counter)
(cond
((null list) counter)
((equal (first list) element)
(recurse (rest list) (1+ counter)))
(t (recurse (rest list) counter)))))
(recurse list 0)))
这里recurse是LABELS引入的局部递归函数,它接受counter参数。与原始递归函数的区别在于,当列表为空时,它返回counter 的当前值而不是零。在这里,recurse 的结果始终与递归调用返回的值相同:编译器只需重新绑定输入并执行跳转,而不是分配中间帧。
高阶函数
这里还有另外两种基于高阶函数的方法。
首先,使用累加器定义函数的常用方法是使用REDUCE(在其他语言中称为fold)。没有明确的突变:
(defun times (list element)
(reduce (lambda (counter value)
(if (equal value element)
(1+ counter)
counter))
list
:initial-value 0))
匿名函数接受累加器的当前状态,列表中访问的当前值,并计算累加器的下一个状态(计数器)。
或者,使用nil 第一个参数调用MAP,以便仅针对效果进行迭代。由LAMBDA 形式建立的匿名函数关闭本地counter 变量,并且可以在比较成立时递增它。它类似于前面的 dolist 示例 w.r.t。通过副作用增加计数器,但迭代是通过 map 隐式完成的。
(defun times (list element)
(let ((counter 0))
(map ()
(lambda (value)
(when (equal value element)
(incf counter)))
list)
counter))
内置
供您参考,有一个内置的COUNT 函数:
(defun times (list element)
(count element list :test #'equal))