我已经完成了Graham Common Lisp第5章练习5,它需要一个带有对象X和向量V的函数,并返回一个紧接在V之前的X之前的所有对象的列表。
它的工作原理如下:
> (preceders #\a "abracadabra")
(#\c #\d #r)
我已经完成了递归版本:
(defun preceders (obj vec &optional (result nil) &key (startt 0))
(let ((l (length vec)))
(cond ((null (position obj vec :start startt :end l)) result)
((= (position obj vec :start startt :end l) 0)
(preceders obj vec result
:startt (1+ (position obj vec :start startt :end l))))
((> (position obj vec :start startt :end l) 0)
(cons (elt vec (1- (position obj vec :start startt :end l)))
(preceders obj vec result
:startt (1+ (position obj vec
:start startt
:end l))))))))
它运作正常,但我的老师给了我以下批评:
“这会反复调用长度。对于向量来说并不是那么糟糕,但仍然没有必要。更高效和更灵活(对用户而言)代码就像定义其他序列处理函数一样。使用:start和:end关键字参数,其他序列函数使用相同的默认初始值。长度最多需要调用一次。“
我正在咨询Common Lisp教科书和谷歌,但似乎没有什么帮助:我不知道他的意思是“使用:start和:end关键字参数”,我不知道如何“只调用一次”。如果你们能让我知道如何改进我的代码以满足我老师发布的要求,我将不胜感激。非常感谢!
更新:
大家好,我现在提出以下代码:
(defun preceders (obj vec
&optional (result nil)
&key (start 0) (end (length vec)) (test #'eql))
(let ((pos (position obj vec :start start :end end :test test)))
(cond ((null pos) result)
((zerop pos) (preceders obj vec result
:start (1+ pos) :end end :test test))
(t (preceders obj vec (cons (elt vec (1- pos)) result)
:start (1+ pos) :end end :test test)))))
我得到了批评:
“当你有一个在多个分支中重复相同的复杂递归调用时,首先调用 ,将它保存在局部变量中,然后在变量中使用变量通常更简单更简单的IF或COND。“
另外,对于我的函数的迭代版本:
(defun preceders (obj vec)
(do ((i 0 (1+ i))
(r nil (if (and (eql (aref vec i) obj)
(> i 0))
(cons (aref vec (1- i)) r)
r)))
((eql i (length vec)) (reverse r))))
我得到批评
“在更好的位置启动DO并删除重复的> 0测试”
请您与我分享您的想法,我认为这是我迈向成功的最后一步! 非常感谢!
答案 0 :(得分:8)
这种函数的典型参数列表是:
(defun preceders (item vector
&key (start 0) (end (length vector))
(test #'eql))
...
)
如您所见,它有START和END参数。
TEST是默认的比较功能。使用(funcall测试项目(aref vector i))。 通常还有一个KEY参数...
每次PRECEDERS的递归调用都会重复调用LENGTH。
我会做非递归版本并在向量上移动两个索引:一个用于第一个项目,另一个用于下一个项目。每当下一个项目是您要查找的项目的EQL时,请将第一个项目推送到结果列表(如果它不是那里的成员)。
对于递归版本,我会写一个由PRECEDERS调用的第二个函数,它接受两个以0和1开头的索引变量,并使用它。我不打电话给POSITION。通常这个函数是通过PRECEDERS中的LABELS的本地函数,但为了使它更容易编写,辅助函数也可以在外面。
(defun preceders (item vector
&key (start 0) (end (length vector))
(test #'eql))
(preceders-aux item vector start end test start (1+ start) nil))
(defun preceders-aux (item vector start end test pos0 pos1 result)
(if (>= pos1 end)
result
...
))
这有帮助吗?
这是使用LOOP的迭代版本:
(defun preceders (item vector
&key (start 0) (end (length vector))
(test #'eql))
(let ((result nil))
(loop for i from (1+ start) below end
when (funcall test item (aref vector i))
do (pushnew (aref vector (1- i)) result))
(nreverse result)))
答案 1 :(得分:5)
由于你已经有了一个有效的解决方案,我将放大Rainer Joswig's solution,主要是为了做出相关的风格评论。
(defun preceders (obj seq &key (start 0) (end (length seq)) (test #'eql))
(%preceders obj seq nil start end test))
使用单独的辅助函数(我称之为%PRECEDERS
,一个表示函数是“私有”的常用约定)的主要原因是消除结果的可选参数。一般来说使用可选参数很好,但可选和关键字参数可以一起播放,并且在一个函数中同时使用它们是创建各种难以调试错误的极其有效的方法。
关于是否使辅助函数全局(使用DEFUN
)或本地(使用LABELS
),这是一个品味问题。我更喜欢将其设为全局,因为它意味着更少的缩进和更容易的交互式调试。 YMMV。
辅助函数的一种可能实现是:
(defun %preceders (obj seq result start end test)
(let ((pos (position obj seq :start start :end end :test test)))
;; Use a local binding for POS, to make it clear that you want the
;; same thing every time, and to cache the result of a potentially
;; expensive operation.
(cond ((null pos) (delete-duplicates (nreverse result) :test test))
((zerop pos) (%preceders obj seq result (1+ pos) end test))
;; I like ZEROP better than (= 0 ...). YMMV.
(t (%preceders obj seq
(cons (elt seq (1- pos)) result)
;; The other little bit of work to make things
;; tail-recursive.
(1+ pos) end test)))))
此外,在所有这些之后,我想我应该指出,我也同意Rainer建议使用显式循环而不是递归,前提是递归执行它不是练习的一部分。
编辑:我切换到辅助函数的更常见的“%”约定。通常,您使用的任何约定都会增加以下事实:您只显式导出构成公共接口的函数,但某些标准函数和宏使用尾随“*”来表示变体功能。
我使用标准DELETE-DUPLICATES
函数更改了删除重复的前置任务的内容。这可能比重复使用ADJOIN
或PUSHNEW
更快(即指数),因为它可以在内部使用散列集表示,至少对于像{{1}这样的常见测试函数},EQ
和EQL
。
答案 2 :(得分:2)
Rainer循环版本的略微修改变体:
(defun preceders (item vector
&key (start 0) (end (length vector))
(test #'eql))
(delete-duplicates
(loop
for index from (1+ start) below end
for element = (aref vector index)
and previous-element = (aref vector (1- index)) then element
when (funcall test item element)
collect previous-element)))
这样可以更多地使用循环指令,除此之外只能访问向量中的每个元素一次(我们将前一个元素保留在前一个元素变量中)。
答案 3 :(得分:1)
回答第一次更新。
第一个问题:
看到这个
(if (foo)
(bar (+ 1 baz))
(bar baz))
这与:
相同(bar (if (foo)
(+ 1 baz)
baz))
或:
(let ((newbaz (if (foo)
(+ 1 baz)
baz)))
(bar newbaz))
第二
为什么不从I = 1开始?
另见我的其他答案中的迭代版本......
答案 4 :(得分:1)
Rainer提出的迭代版本非常好,它紧凑且效率更高,因为您只遍历序列一次; 与在每次迭代时调用(编辑:对不起,我是关于这最后一句完全错了,请参阅Rainer的评论)position
并因此每次遍历子序列的递归版本形成对比。
如果需要递归版本,另一种方法是推进start
直到它遇到end
,并沿途收集结果。
(defun precede (obj vec &key (start 0) (end (length vec)) (test #'eql))
(if (or (null vec) (< end 2)) nil
(%precede-recur obj vec start end test '())))
(defun %precede-recur (obj vec start end test result)
(let ((next (1+ start)))
(if (= next end) (nreverse result)
(let ((newresult (if (funcall test obj (aref vec next))
(adjoin (aref vec start) result)
result)))
(%precede-recur obj vec next end test newresult)))))
当然,这只是表达loop
版本的另一种方式。
试验:
[49]> (precede #\a "abracadabra")
(#\r #\c #\d)
[50]> (precede #\a "this is a long sentence that contains more characters")
(#\Space #\h #\t #\r)
[51]> (precede #\s "this is a long sentence that contains more characters")
(#\i #\Space #\n #\r)
另外,我对罗伯特感兴趣,您的老师是否说他不喜欢在递归算法中使用adjoin
或pushnew
?