列表和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
答案 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]