惯用效率高的Haskell追加?

时间:2011-03-04 00:21:41

标签: haskell performance linked-list append idiomatic

列表和cons运算符(:)在Haskell中非常常见。缺点是我们的朋友。但有时我想添加到列表的末尾。

xs `append` x = xs ++ [x]

遗憾的是,这不是 实现它的有效方式。

我在Haskell中写了Pascal's triangle,但我不得不使用++ [x]反成语:

ptri = [1] : mkptri ptri
mkptri (row:rows) = newRow : mkptri rows
    where newRow = zipWith (+) row (0:row) ++ [1]
imho,这是一个可爱的可读Pascal的三角形和所有,但反成语让我烦恼。有人可以向我解释(并且,理想情况下,指向一个很好的教程)关于您想要有效追加到最后的情况下的惯用数据结构吗?我希望这个数据结构及其方法具有近似列表般的美感。或者,或者,向我解释为什么这个反成语对于这种情况实际上并不坏(如果你认为是这样的话)。


[编辑]我最喜欢的答案是Data.Sequence,它确实具有“接近列表的美丽”。不确定我对操作所要求的严格程度。我们随时欢迎进一步的建议和不同的想法。

import Data.Sequence ((|>), (<|), zipWith, singleton)
import Prelude hiding (zipWith)

ptri = singleton 1 : mkptri ptri

mkptri (seq:seqs) = newRow : mkptri seqs
    where newRow = zipWith (+) seq (0 <| seq) |> 1

现在我们只需要List成为一个类,这样其他结构就可以使用像zipWith这样的方法,而不会将它从Prelude中隐藏,或者对它进行限定。 :P

12 个答案:

答案 0 :(得分:27)

请记住,看起来很糟糕的渐近现实可能并非如此,因为您使用的是懒惰的语言。在严格的语言中,以这种方式附加到链表的末尾将始终为O(n)。在一种懒惰的语言中,只有当你实际遍历到列表的末尾时它才是O(n),在这种情况下,无论如何你都会花费O(n)的努力。所以在很多情况下,懒惰可以拯救你。

这不是一个保证......例如,k追加后跟遍历仍然会在O(nk)中运行,它可能是O(n + k)。但它确实改变了这种情况。当结果立即被强制时,考虑单个操作在渐近复杂度方面的性能并不总是能给出正确的答案。

答案 1 :(得分:16)

标准Sequence从“两端”添加O(1)和O(log(min(n1,n2)))用于一般连接:

http://hackage.haskell.org/packages/archive/containers/latest/doc/html/Data-Sequence.html

与列表的区别在于Sequence是严格的

答案 2 :(得分:10)

像这样的显式递归会避免你追加“反成语”。虽然,我认为它不如你的例子那么清楚。

ptri = []:mkptri ptri
mkptri (xs:ys) = pZip xs (0:xs) : mkptri ys
    where pZip (x:xs) (y:ys) = x+y : pZip xs ys
          pZip [] _ = [1]

答案 3 :(得分:8)

在Pascal三角形的代码中,++ [x]实际上并不是问题。既然你必须在++的左侧产生一个新的列表,你的算法本质上是二次的;只是通过避免使用++,你无法使其渐近更快。

此外,在这种特殊情况下,当你编译-O2时,GHC的列表融合规则(应该)会删除++通常会创建的列表的副本。这是因为zipWith是一个很好的制作人,而++是第一个参数的好消费者。您可以在GHC User's Guide中了解这些优化。

答案 4 :(得分:5)

根据您的使用案例,ShowS方法(通过函数组合追加)可能很有用。

答案 5 :(得分:5)

如果你只想要便宜的附加(concat)和snoc(右边的cons),一个Hughes列表,也称为Hackist上的DList,是最简单的实现。如果你想知道它们是如何工作的,看看Andy Gill和Graham Hutton的第一篇工人包装纸,John Hughes的原始论文似乎并不在线。正如其他人所说的那样,ShowS是一个String专门的Hughes列表/ DList。

JoinList需要更多工作才能实现。这是一个二叉树,但有一个列表API - concat和snoc很便宜,你可以合理地映射它:Hackage上的DList有一个functor实例,但我认为它不应该 - functor实例必须变换进出变换器常规清单。如果你想要一个JoinList,那么你需要自己动手 - Hackage上的那个是我的,它效率不高,写得不好。

Data.Sequence具有有效的缺点和snoc,并且适用于其他操作 - 取消,丢弃等等,JoinList很慢。因为Data.Sequence的内部指纹树实现必须平衡树,所以追加比其JoinList等效更多的工作。实际上,因为Data.Sequence编写得更好,我希望它仍然超出我的JoinList for append。

答案 6 :(得分:4)

另一种方法是通过使用无限列表来避免连接:

ptri = zipWith take [0,1..] ptri'
  where ptri' = iterate stepRow $ repeat 0
        stepRow row = 1 : zipWith (+) row (tail row)

答案 7 :(得分:3)

我不一定会将您的代码称为“反自我”。通常,更清晰更好,即使这意味着牺牲几个时钟周期。

在您的特定情况下,最后的追加并不会实际改变big-O 时间复杂度!评估表达式

zipWith (+) xs (0:xs) ++ [1]

将花费时间与length xs成比例,并且没有花哨的序列数据结构会改变它。如果有的话,只会影响常数因素。

答案 8 :(得分:2)

Chris Okasaki有一个解决此问题的队列设计。见他的论文的第15页 http://www.cs.cmu.edu/~rwh/theses/okasaki.pdf

您可能需要略微调整代码,但有些使用反向并保留两个列表可以让您更有效地工作平均

此外,有人在monad阅读器中提供了一些列表代码并进行了有效的操作。我承认,我并没有真正遵循它,但我想如果我集中注意力,我就能搞清楚。原来是道格拉斯M.奥克莱尔在莫纳德读者问题17中 http://themonadreader.files.wordpress.com/2011/01/issue17.pdf


我意识到上述答案没有直接解决这个问题。所以,对于咯咯笑,这是我的递归答案。随意撕开它 - 它不漂亮。

import Data.List 

ptri = [1] : mkptri ptri

mkptri :: [[Int]] -> [[Int]]
mkptri (xs:ys) =  mkptri' xs : mkptri ys

mkptri' :: [Int] -> [Int]
mkptri' xs = 1 : mkptri'' xs

mkptri'' :: [Int] -> [Int]
mkptri'' [x]        = [x]
mkptri'' (x:y:rest) = (x + y):mkptri'' (y:rest)

答案 9 :(得分:1)

我写了一个@ geekosaur ShowS方法的例子。您可以在prelude中看到ShowS的许多示例。

ptri = []:mkptri ptri
mkptri (xs:ys) = (newRow xs []) : mkptri ys

newRow :: [Int] -> [Int] -> [Int]
newRow xs = listS (zipWith (+) xs (0:xs)) . (1:)

listS :: [a] -> [a] -> [a]
listS [] = id
listS (x:xs) = (x:) . listS xs

[编辑]作为@Dan的想法,我用zipWithS改写了newRow。

newRow :: [Int] -> [Int] -> [Int]
newRow xs = zipWithS (+) xs (0:xs) . (1:)

zipWithS :: (a -> b -> c) -> [a] -> [b] -> [c] -> [c]
zipWithS z (a:as) (b:bs) xs =  z a b : zipWithS z as bs xs
zipWithS _ _ _ xs = xs

答案 10 :(得分:1)

如果您正在寻找通用解决方案,那么如何:

mapOnto :: [b] -> (a -> b) -> [a] -> [b]
mapOnto bs f = foldr ((:).f) bs

这为map提供了一个简单的替代定义:

map = mapOnto []

我们可以对其他基于foldr的函数进行类似的定义,比如zipWith:

zipOntoWith :: [c] -> (a -> b -> c) -> [a] -> [b] -> [c]
zipOntoWith cs f = foldr step (const cs)
  where step x g [] = cs
        step x g (y:ys) = f x y : g ys

再次轻松地推导出zipWith和zip:

zipWith = zipOntoWith []
zip = zipWith (\a b -> (a,b))

现在,如果我们使用这些通用功能,那么您的实现 变得非常简单:

ptri :: (Num a) => [[a]]
ptri = [] : map mkptri ptri
  where mkptri xs = zipOntoWith [1] (+) xs (0:xs)

答案 11 :(得分:1)

您可以将列表表示为从[]

构建列表的函数
list1, list2 :: [Integer] -> [Integer]
list1 = \xs -> 1 : 2 : 3 : xs
list2 = \xs -> 4 : 5 : 6 : xs

然后,您可以轻松追加列表并添加到任一端。

list1 . list2 $ [] -> [1,2,3,4,5,6]
list2 . list1 $ [] -> [4,5,6,1,2,3]
(7:) . list1 . (8:) . list2 $ [9] -> [7,1,2,3,8,4,5,6,9]

您可以重写zipWith以返回这些部分列表:

zipWith' _ [] _ = id
zipWith' _ _ [] = id
zipWith' f (x:xs) (y:ys) = (f x y :) . zipWith' f xs ys

现在你可以写ptri:

ptri = [] : mkptri ptri
mkptri (xs:yss) = newRow : mkptri yss
    where newRow = zipWith' (+) xs (0:xs) [1]

进一步说,这是一个更加对称的单线:

ptri = ([] : ) . map ($ []) . iterate (\x -> zipWith' (+) (x [0]) (0 : x [])) $ (1:)

或者这更简单:

ptri = [] : iterate (\x -> 1 : zipWith' (+) (tail x) x [1]) [1]

或没有zipWith'(mapAccumR在Data.List中):

ptri = [] : iterate (uncurry (:) . mapAccumR (\x x' -> (x', x+x')) 0) [1]