在函数内部删除+ SETF

时间:2015-10-29 19:36:25

标签: 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-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无法正常工作(例如,如果我使用REMOVEFOO根本不会改变。

是否有任何我不知道的范围规则?

3 个答案:

答案 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所说:

  

deletedelete-ifdelete-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维护旧值。