为什么这个简单的O(n)Haskell算法表现得更像O(2 ^ n)?

时间:2016-01-25 05:16:38

标签: algorithm haskell caching big-o time-complexity

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进行计算。

1 个答案:

答案 0 :(得分:7)

您的版本非常慢的原因是lucas (n-1)lucas (n-2)部分没有 memoization - 所以这两个值都会一遍又一遍地重新计算(递归)再次 - 这是非常缓慢的。

解决方案是将计算值保留在某处:

使用list-lazyness

这是一个简单的版本,与您的代码片段相同,但应该更快 - 它会将已经计算的部分保留在列表中:

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版本