我试图实现一个Haskell函数,它将整数数组A作为输入 并产生另一个数组B = [A [0],A [0] + A [1],A [0] + A [1] + A [2],...]。我知道Data.List中的scanl可以用于函数(+)。我写了第二个实现 (在看到scanl的源代码后,它执行得更快)。我想知道为什么第一个实现比第二个实现慢,尽管它是尾递归的?
-- This function works slow.
ps s x [] = x
ps s x y = ps s' x' y'
where
s' = s + head y
x' = x ++ [s']
y' = tail y
-- This function works fast.
ps' s [] = []
ps' s y = [s'] ++ (ps' s' y')
where
s' = s + head y
y' = tail y
有关上述代码的一些细节:
实施1:应该称为
ps 0 [] a
其中'a'是你的数组。
实施2:应该称为
ps' 0 a
其中'a'是你的数组。
答案 0 :(得分:6)
您正在改变++
关联的方式。在您的第一个函数中,您正在计算((([a0] ++ [a1]) ++ [a2]) ++ ...)
,而在第二个函数中,您正在计算[a0] ++ ([a1] ++ ([a2] ++ ..))
。在列表的开头添加一些元素是O(1)
,而在列表的末尾附加一些元素是O(n)
。这导致了整体的线性与二次算法。
您可以通过以相反的顺序构建列表,然后在最后再次反转,或使用dlist之类的东西来修复第一个示例。然而,对于大多数目的而言,第二个仍然会更好。虽然尾调用确实存在并且在Haskell中很重要,但如果你熟悉一种严格的函数式语言,比如Scheme或ML,你对如何以及何时使用它们的直觉是完全错误的。
第二个例子在很大程度上更好,因为它是增量的;它立即开始返回消费者可能感兴趣的数据。如果你刚刚使用双反向或dlist技巧修复了第一个例子,你的函数将在它返回任何内容之前遍历整个列表。
答案 1 :(得分:1)
我想提一下,您的功能可以更容易地表达为
drop 1 . scanl (+) 0
通常,最好使用预定义的组合器,如scanl
,以支持编写自己的递归方案;它提高了可读性,减少了你不必要地挥霍性能的可能性。
但是,在这种情况下,由于延迟评估,我的scanl
版本和原始ps
以及ps'
有时会导致堆栈溢出:Haskell不一定会立即评估添加内容(取决于严格性分析)。
如果您执行last (ps' 0 [1..100000000])
,则可以看到这种情况。这会导致堆栈溢出。您可以通过强制Haskell立即评估添加来解决该问题,例如通过定义您自己的严格scanl
:
myscanl :: (b -> a -> b) -> b -> [a] -> [b]
myscanl f q [] = []
myscanl f q (x:xs) = q `seq` let q' = f q x in q' : myscanl f q' xs
ps' = myscanl (+) 0
然后,调用last (ps' [1..100000000])
。