在阅读了关于 persistence 的文章后,在Okasaki的纯功能数据结构中阅读了关于单链表的说明性例子(这就是Haskell&#39 ; s列表已实施),我想知道Data.List
inits
tails
和tails
...
在我看来
inits
的空间复杂度在其参数的长度上线性,并且tails
的空间复杂度在其参数长度中二次,但是一个简单的基准表示不然。
使用tails xs
,可以共享原始列表。计算xs
只需沿着列表xs
行走,并创建一个指向该列表中每个元素的新指针;无需在内存中重新创建inits xs
的一部分。
相反,因为xs
"的每个元素以不同的方式结束",所以不能有这样的共享,并且必须从-- Main.hs
import Data.List (inits, tails)
main = do
let intRange = [1 .. 10 ^ 4] :: [Int]
print $ sum intRange
print $ fInits intRange
print $ fTails intRange
fInits :: [Int] -> Int
fInits = sum . map sum . inits
fTails :: [Int] -> Int
fTails = sum . map sum . tails
重新创建所有可能的前缀在记忆中划伤。
下面的简单基准测试显示,两个功能之间的内存分配没有太大区别:
Main.hs
用
编译我的ghc -prof -fprof-auto -O2 -rtsopts Main.hs
文件后
./Main +RTS -p
并正在运行
Main.prof
COST CENTRE MODULE %time %alloc
fInits Main 60.1 64.9
fTails Main 39.9 35.0
文件报告以下内容:
fInits
为fTails
分配的内存和为tails
分配的内存具有相同的数量级......哼......
inits
(线性)和fInits
(二次)空间复杂度的结论是否正确?fTails
和{{1}}分配大致相同的内存? list fusion 与此有关吗?答案 0 :(得分:2)
Haskell报告中inits
的实现与基本4.7.0.1(GHC 7.8.3)的实现相同或几乎完全相同,速度非常慢。特别是,fmap
应用程序递归堆叠,因此强制结果的连续元素变得越来越慢。
inits [1,2,3,4] = [] : fmap (1:) (inits [2,3,4])
= [] : fmap (1:) ([] : fmap (2:) (inits [3,4]))
= [] : [1] : fmap (1:) (fmap (2:) ([] : fmap (3:) (inits [4])))
....
Bertram Felgenhauer探索的最简单的渐近最优实现是基于将take
应用于相继更大的参数:
inits xs = [] : go (1 :: Int) xs where
go !l (_:ls) = take l xs : go (l+1) ls
go _ [] = []
Felgenhauer使用take
的私有非融合版本能够获得额外的性能,但它仍然没有尽可能快。
以下非常简单的实现在大多数情况下要快得多:
inits = map reverse . scanl (flip (:)) []
在一些奇怪的极端情况下(如map head . inits
),这个简单的实现是渐近非最优的。因此,我使用相同的技术编写了一个版本,但基于Chris Okasaki的Banker队列,这是渐近最优和几乎同样快的。 Joachim Breitner进一步优化了它,主要是使用严格的scanl'
而不是通常的scanl
,这个实现进入了GHC 7.8.4。 inits
现在可以在O(n)时间内产生结果的脊柱;强制整个结果需要O(n ^ 2)时间,因为在不同的初始段之间不能共享任何一个。如果你想要真正荒谬的inits
和tails
,最好的办法是使用Data.Sequence
; Louis Wasserman的实施is magical。另一种可能性是使用Data.Vector
- 它可能会使用切片来处理这类事情。