这是一个非常懒惰的版本,适合处理无限列表。第一个之后的每个结果列表的每个元素只需要O(1) 摊销时间来计算它,无论我们查看列表有多远。
总体思路是:对于每个长度n,我们打算将列表拆分为长度为n 的项目队列和列表的其余部分。为了产生结果,我们首先检查列表中是否有另一个项目可以在队列中占有一席之地,然后产生队列中的第一个项目。当我们到达列表的末尾时,我们会从队列中丢弃剩余的项目。
import Data.Sequence (Seq, empty, fromList, ViewL (..), viewl, (|>))
starts :: [a] -> [[a]]
starts = map (uncurry shiftThrough) . splits
shiftThrough :: Seq a -> [a] -> [a]
shiftThrough queue [] = []
shiftThrough queue (x:xs) = q1:shiftThrough qs xs
where
(q1 :< qs) = viewl (queue |> x)
splits 查找列表的所有初始序列以及尾列表。
splits :: [a] -> [(Seq a, [a])]
splits = go empty
where
go s [] = []
go s (x:xs) = (s,x:xs):go (s |> x) xs
我们可以用相同的策略写出从列表末尾删除。
dropEnd :: Int -> [a] -> [a]
dropEnd n = uncurry (shiftThrough . fromList) . splitAt n
这些使用Data.Sequence 的摊销O(n) 构造一个序列fromList,O(1) 用|> 和O(1) 附加到序列的末尾,用viewl 检查序列的开头。
这足以在几秒钟内快速查询(starts [1..]) !! 80000 和(starts [1..]) !! 8000000 之类的内容。
看,没有进口
队列的一个简单的纯函数实现是一对列表,一个包含要按顺序输出的事物next,另一个包含最近的事物added。每当添加某些内容时,它都会添加到 added 列表的开头。当需要某些东西时,该项目将从next 列表的开头删除。当next 列表中没有剩余要删除的项目时,它会以相反的顺序被added 列表替换,并且added 列表设置为[]。这已经摊销了 O(1) 运行时间,因为每个项目将被添加一次、删除一次和反转一次,但是许多反转将同时发生。
delay 使用上面描述的队列逻辑来实现与上一节中的shiftThrough 相同的事情。 xs 是最近使用的事物列表 added 和 ys 是要使用的事物列表 next。
delay :: [a] -> [a] -> [a]
delay ys = traverse step ([],ys)
where
step (xs, ys) x = step' (x:xs) ys
step' xs [] = step' [] (reverse xs)
step' xs (y:ys) = (y, (xs, ys))
traverse 差点被扫描了
traverse :: (s -> a -> (b, s)) -> s -> [a] -> [b]
traverse f = go
where
go _ [] = []
go s (x:xs) = y : go s' xs
where (y, s') = f s x
我们可以根据delay 和返回列表的splits 的另一个版本来定义starts。
starts :: [a] -> [[a]]
starts = map (uncurry delay) . splits
splits :: [a] -> [([a], [a])]
splits = go []
where
go s [] = []
go s (x:xs) = (reverse s, x:xs):go (x:s) xs
这与使用Seq 的实现具有非常相似的性能。