我试图编写一个破坏性地从列表中删除N
元素并返回它们的函数。我提出的代码(见下文)看起来很好,除了SETF
没有按照我的意图工作。
(defun pick (n from)
"Deletes (destructively) n random items from FROM list and returns them"
(loop with removed = nil
for i below (min n (length from)) do
(let ((to-delete (alexandria:random-elt from)))
(setf from (delete to-delete from :count 1 :test #'equal)
removed (nconc removed (list to-delete))))
finally (return removed)))
对于大多数情况,这很好用:
CL-USER> (defparameter foo (loop for i below 10 collect i))
CL-USER> (pick 3 foo)
(1 3 6)
CL-USER> foo
(0 2 4 5 7 8 9)
CL-USER> (pick 3 foo)
(8 7 0)
CL-USER> foo
(0 2 4 5 9)
正如您所看到的,PICK
工作得很好(在SBCL上),除非被选中的元素恰好是列表中的第一个元素。在这种情况下,它不会被删除。这是因为唯一的重新分配是在DELETE
内进行的重新分配。 SETF
无法正常工作(例如,如果我使用REMOVE
,FOO
根本不会改变。
是否有任何我不知道的范围规则?
答案 0 :(得分:5)
一个(正确的)列表由cons单元组成,每个cons单元都包含对下一个的引用
细胞。所以,它实际上是一个引用链,而你的变量有一个
引用第一个单元格。为了清楚起见,我将绑定重命名为外部
你的职能是var
:
var ---> [a|]--->[b|]--->[c|nil]
当您将变量的值传递给函数时,参数将获得 绑定到相同的参考。
var ---> [a|]--->[b|]--->[c|nil]
/
from --'
您可以更新链中的引用,例如消除b
:
var ---> [a|]--->[c|nil]
/
from --'
这会对var
在外面看到的列表产生影响。
如果您更改了第一个引用,例如消除a
,则只是
一个来自from
:
var ---> [a|]--->[b|]--->[c|nil]
/
from --'
这显然对var
看到的内容没有影响。
您需要实际更新有问题的变量绑定。你可以做到这一点 通过将其设置为函数返回的值。既然你已经退货了 不同的值,这将是一个额外的返回值。
(defun pick (n list)
(;; … separate picked and rest, then
(values picked rest)))
然后你可以这样使用,例如:
(let ((var (list 1 2 3)))
(multiple-value-bind (picked rest) (pick 2 var)
(setf var rest)
(do-something-with picked var)))
现在分离:除非名单太长,否则我会坚持
非破坏性的操作。我也不会使用random-elt
,因为它需要
每次遍历 O(m)元素( m 是列表的大小),
导致运行时 O(n·m)。
通过确定当前的拣选机会,您可以获得 O(m)整体运行时间 在列表上线性运行时的当前项。然后你收集 将项目放入已选择或休息列表中。
(defun pick (n list)
(loop :for e :in list
:and l :downfrom (length list)
:when (or (zerop n)
(>= (random 1.0) (/ n l)))
:collect e :into rest
:else
:collect e :into picked
:and :do (decf n)
:finally (return (values picked rest))))
答案 1 :(得分:3)
删除不是必需来修改任何结构,它只是允许。实际上,您不能总是进行破坏性删除。如果你想从(42)中删除42,你需要返回空列表(),这是符号NIL,但你无法转动列表(42),这是一个利弊单元(42) .NIL)成为不同类型的对象(符号NIL)。因此,您可能需要返回更新的列表以及已删除的元素。你可以用这样的东西做到这一点,它会返回多个值:
(defun pick (n from)
(do ((elements '()))
((or (endp from) (zerop n))
(values elements from))
(let ((element (alexandria:random-elt from)))
(setf from (delete element from)
elements (list* element elements))
(decf n))))
CL-USER> (pick 3 (list 1 2 3 2 3 4 4 5 6))
(2 6 4)
(1 3 3 5)
CL-USER> (pick 3 (list 1 2 3 4 5 6 7))
(2 7 5)
(1 3 4 6)
CL-USER> (pick 2 (list 1 2 3))
(2 3)
(1)
CL-USER> (pick 2 (list 1))
(1)
NIL
在接收端,你会想要使用像multiple-value-bind或multiple-value-setq这样的东西:
(let ((from (list 1 2 3 4 5 6 7)))
(multiple-value-bind (removed from)
(pick 2 from)
(format t "removed: ~a, from: ~a" removed from)))
; removed: (7 4), from: (1 2 3 5 6)
(let ((from (list 1 2 3 4 5 6 7))
(removed '()))
(multiple-value-setq (removed from) (pick 2 from))
(format t "removed: ~a, from: ~a" removed from))
; removed: (3 5), from: (1 2 4 6 7)
答案 2 :(得分:2)
delete
不一定修改其序列参数。正如hyperspec所说:
delete
,delete-if
和delete-if-not
返回与具有相同元素的序列相同类型的序列,但由start
和{所限定的子序列中的序列除外{1}}并且满足测试已被删除。可以销毁序列并用于构造结果;但是,结果可能与序列相同或不同。
例如,在SBCL:
end
请注意,在您的函数* (defvar f (loop for i below 10 collect i))
F
* (defvar g (delete 0 f :count 1 :test #'equal))
G
* g
(1 2 3 4 5 6 7 8 9)
* f
(0 1 2 3 4 5 6 7 8 9)
*
中修改局部变量setf
,并且因为from
在第一个元素的情况下不修改原始列表,所以在函数末尾变量delete
维护旧值。