Haskell缓存纯函数调用的结果,这是纯粹和不纯行为分离的众多原因之一。然而,这个函数应该在O(n)中运行,其中n是50
,运行得非常慢:
lucas 1 = 1
lucas 2 = 3
lucas n = lucas (n-1) + lucas (n-2)
map (lucas) [1..50]
前三十个左右一起计算在一秒以下,然后31需要半秒左右,32需要一秒钟,33需要几秒钟,34需要6秒,35需要11秒,36需要17秒...
为什么这个功能这么慢?如果GHC交互式运行时缓存结果,则对lucas
的每次调用应仅涉及前两个缓存术语的求和。一个额外的操作找到术语3,一个额外的添加找到术语4,一个额外的添加找到术语5,所以术语50达到考虑缓存只有48个加法。这个功能不应该花费近一秒的时间才能找到至少前几千个术语,为什么表现如此糟糕?
我已经等了半个多小时才能lucas 500
进行计算。
答案 0 :(得分:7)
您的版本非常慢的原因是lucas (n-1)
和lucas (n-2)
部分没有 memoization - 所以这两个值都会一遍又一遍地重新计算(递归)再次 - 这是非常缓慢的。
解决方案是将计算值保留在某处:
这是一个简单的版本,与您的代码片段相同,但应该更快 - 它会将已经计算的部分保留在列表中:
lucasNumbers :: [Integer]
lucasNumbers = 1:3:zipWith (+) lucasNumbers (tail lucasNumbers)
first50 :: [Integer]
first50 = take 50 lucasNumbers
这个更快的原因是现在列表的懒惰会帮助你记住不同的部分
如果你寻找Fibonacci sequences in Haskell(你的确和你的一样),你可以学到很多东西;)
unfoldr
另一种(可能更少魔法看似)这样做的方法是使用Data.List.unfoldr
- 这里已经计算过的部分(或那些重要的部分 - 最后和倒数第二个元素)将处于状态您传递展开操作:
lucasNumbers :: [Integer]
lucasNumbers = unfoldr (\ (n,n') -> Just (n, (n',n+n'))) (1,3)
发表评论/问题:
假设您正在谈论x(n) = x(n-1)^2-2
,那么您可以这样做:
lucasLehmer :: [Integer]
lucasLehmer = 4 : map (\ x -> x^2-2) lucasLehmer
会产生这样的结果:
λ> take 5 lucasLehmer
[4,14,194,37634,1416317954]
也许您应该自己尝试unfoldr
版本