Lisp 删除+;函数中的SETF

Lisp 删除+;函数中的SETF,lisp,common-lisp,Lisp,Common Lisp,我正在尝试编写一个函数,它将从列表中破坏性地删除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-d

我正在尝试编写一个函数,它将从列表中破坏性地删除
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
根本不会改变)


是否存在我不知道的任何作用域规则?

delete
不一定修改其序列参数。正如政府所说:

delete
delete if
delete if not
返回一个与序列类型相同的序列,该序列具有相同的元素,但以
start
end
为界且满足测试的子序列中的元素已被删除。序列可能被破坏并用于构建结果;然而,结果可能与序列相同,也可能不相同

例如,在SBCL中:

* (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
中,修改来自
的局部变量
,并且由于
delete
在第一个元素的情况下不会修改原始列表,因此在函数末尾,变量
foo
会保留旧值。

修改任何结构都不需要delete,这是允许的。事实上,你不能总是进行破坏性的删除。如果要从(42)中删除42,则需要返回空列表(),即符号NIL,但无法将列表(42),即cons单元格(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))))

在接收端,您需要使用类似于多值绑定或多值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)

(正确的)列表由cons单元格组成,每个单元格保存对下一个单元格的引用 牢房。因此,它实际上是一个引用链,并且您的变量有一个 对第一个单元格的引用。为了说明这一点,我将绑定重命名为outside 将您的函数设置为
var

var ---> [a|]--->[b|]--->[c|nil]
将变量的值传递给函数时,参数 绑定到同一引用

var ---> [a|]--->[b|]--->[c|nil]
        /
from --'
您可以更新链中的引用,例如消除
b

var ---> [a|]--->[c|nil]
        /
from --'
这会影响
var
在外部看到的列表

如果更改第一个引用,例如删除
a
,这只是 源于
的一个:

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)))
现在谈谈分离:除非名单太长,否则我会坚持 非破坏性行动。我也不会使用
随机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))))

所以,我刚刚发现其中似乎有一个类似的问题。不过,我没有使用引用的数据。这里有什么我遗漏的吗?
下面的(minn n(length from))
如果要更改列表的长度,将没有多大意义。您不能编写“从列表中删除(破坏性地)n个随机项并返回它们”的函数。如果要从
(42)
中删除一个随机项,会发生什么情况。你不能把cons单元格变成一个空列表。我知道我在改变列表的长度。我只想确保更改的次数不超过列表本身的长度,因此下面的
子句就是这样。如果我删除它,
循环
将抛出一个错误。其实这并不重要,我只是从一个数百个项目的列表中删除了几十个项目。你可能会发现这些问题和答案很有帮助。这是一个非常简单的例子;如果您只需要从列表中删除第一个元素,最简单的方法就是返回列表的其余部分。如果您尝试以一种没有元素的方式删除,则删除不会具有破坏性。例如,如果您有
(1)
,并且尝试删除
1
,则无法返回同一cons单元格,必须返回
nil
。无法将
(1)
设为空。我知道我不能指望
删除
来更改列表的值。这就是我试图在函数中设置它的原因。问题是,
SETF
似乎在函数之外没有任何效果。刚刚看到您的编辑。因此,
中的
是我函数的局部变量。知道了!“通过在列表上线性运行时确定拾取当前项目的当前机会,可以获得O(m)总体运行时间。然后将项目收集到拾取列表或剩余列表中。”如果列表中包含重复的元素,这将不太正确。@JoshuaTaylor:我不理解指定删除随机选择的元素的所有重复项的要求(即使错误的尝试似乎暗示了这种效果)。你质疑这一点是对的,我或许应该要求澄清,但我敢打赌,删除重复项是实际的
(let ((var (list 1 2 3)))
  (multiple-value-bind (picked rest) (pick 2 var)
    (setf var rest)
    (do-something-with picked var)))
(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))))