(defun slide (list n)
(loop for head on list
for tail = (nthcdr n list) then (cdr tail)
collect (ldiff head tail)
while tail))
测试:
[1]> (slide nil 0)
NIL
[2]> (slide nil 1)
NIL
[3]> (slide nil 2)
NIL
[4]> (slide '(1) 0)
(NIL)
[5]> (slide '(1) 1)
((1))
[6]> (slide '(1) 2)
((1))
[7]> (slide '(1) 3)
((1))
[8]> (slide '(1 2 3) 0)
(NIL NIL NIL)
[9]> (slide '(1 2 3) 1)
((1) (2) (3))
[10]> (slide '(1 2 3) 2)
((1 2) (2 3))
[11]> (slide '(1 2 3) 3)
((1 2 3))
[12]> (slide '(1 2 3) 4)
((1 2 3))
[13]> (slide '(1 2 3) 5)
((1 2 3))
[14]> (slide '(1 2 3 4 5) 3)
((1 2 3) (2 3 4) (3 4 5))
[15]> (slide '(1 2 3 4 5) 4)
((1 2 3 4) (2 3 4 5))
[16]> (slide '(1 2 3 4 5) 5)
((1 2 3 4 5))
[17]> (slide '(1 2 3 4 5) 6)
n 超过列表长度或为零的测试用例超出规范;包括那些只是为了显示实际行为。值 -1 将违反 nthcdr 的约定,因此应发出错误信号,除非编译为低安全性。
它是如何工作的:
-
for head on list 中的on 迭代列表的cons 单元主干,而不是项目。 head 循环变量在第一次迭代时设置为 list。然后在第二次迭代中它转到(cdr head)。这将在nil 被命中时终止。
-
与此迭代并行,我们有for tail = (nthcdr n list) then (cdr tail)。这与for tail on (nthcdr n list) 非常相似。 tail 变量被初始化为列表下方的n 单元格,然后通过cdr 前进。我们不使用for var on list 子句的原因是我们不想在循环变量命中nil 时终止。当tail 变为nil 时,我们希望将nil 值包含在迭代中,因此我们使用loop 的for var = initial-val then step-val 子句打开所需的迭代代码。
-
因此我们有head 和tail 指针在列表中以彼此偏移的方式行进,如图所示,显示列表(1 2 3 4) 以及head 和tail 与@ 的初始状态987654349@是2:
[ 1 | * ] <--- head
|
v
[ 2 | * ]
|
v
[ 3 | * ] <--- tail
|
v
[ 4 | NIL ]
所以在这里,在第一次迭代中,我们将计算 (ldiff head tail)。返回的是head 列表前缀的副本,没有tail 部分。 ldiff 向下移动 head 列表,将项目复制到新列表中,并在遇到 tail 指针时停止复制。如果您理解git,那么(ldiff head tail) 类似于git cherry-pick tail..head。来自head 的项目被挑选到返回列表中,直到tail,这不是挑选的。在此示例中,(ldiff head tail) 产生 (1 2)。 tail 指向(3 4),不包括在内。
-
在每次迭代中,head 和 tail 都在列表中并行向下移动一个单元格,因此 ldiff 将继续计算列表中不同的 n 大小的切片。下次我们会收集(2 3)。
-
在最后一次迭代中,tail 变为 nil。在这种情况下,(ldiff head tail) 的行为就像(copy-list head):它不断地将项目复制到一个新列表中,直到遇到tail。因为那是nil,所以当它到达列表的末尾时会发生这种情况,此时它已经复制了所有的列表。
-
while tail 确保终止。这个子句放在底部,这样我们就有了一个底部测试:我们想在tail 是nil 时包含迭代,然后停止。
另一方面,这里有一个 TXR Lisp 语言的解决方案,它展示了如何通过模式匹配解决这个问题。这很容易被转录以与其他 Lisp 方言及其可用的模式匹配库(如 Common Lisp 的 Trivia)一起使用。
1> (append-each ((tail (conses '(1 2 3 4 5 6 7))))
(if-match (@a @b @c @d . @nil) tail
^((,a ,b ,c ,d))))
((1 2 3 4) (2 3 4 5) (3 4 5 6) (4 5 6 7))
唯一的问题是,我们对参数 n 进行了硬编码,因为它是由模式的长度表示的,这是代码中的一段语法。
在某些情况下,我更喜欢这种解决方案,尤其是当需求以某种方式发生变化时。例如,假设我们只需要收集那些满足某些算术属性的四联体。能够通过a、b、c、d 变量名来引用元组元素会很方便。
我们可以在模式本身中引入约束。例如:只收集以 1 开头的元组:
5> (append-each ((tail (conses '(1 2 1 2 3 1 2 3 4 5 6 1 2 3 4))))
(if-match (1 @b @c @d . @nil) tail
^((1 ,b ,c ,d))))
((1 2 1 2) (1 2 3 1) (1 2 3 4) (1 2 3 4))
另一种方式,保留变量a:
7> (append-each ((tail (conses '(1 2 1 2 3 1 2 3 4 5 6 1 2 3 4))))
(if-match (@(eql 1 @a) @b @c @d . @nil) tail
^((,a ,b ,c ,d))))
((1 2 1 2) (1 2 3 1) (1 2 3 4) (1 2 3 4))
或以奇数结尾的元组:
8> (append-each ((tail (conses '(1 2 1 2 3 1 2 3 4 5 6 1 2 3 4))))
(if-match (@a @b @c @(oddp @d) . @nil) tail
^((,a ,b ,c ,d))))
((2 1 2 3) (1 2 3 1) (3 1 2 3) (2 3 4 5) (4 5 6 1) (6 1 2 3))
或使用保护表达式(通过此模式匹配器中的require 运算符)仅收集最后两个元素相加为五的元组:
9> (append-each ((tail (conses '(1 2 1 2 3 1 2 3 4 5 6 1 2 3 4))))
(if-match @(require (@a @b @c @d . @nil)
(= 5 (+ c d)))
tail
^((,a ,b ,c ,d))))
((2 1 2 3) (3 1 2 3) (6 1 2 3))