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

Why is this simple O(n) Haskell algorithm behaving more like O(2^n)?

Haskell 缓存纯函数调用的结果,这是区分纯行为和不纯行为的众多原因之一。然而这个函数,应该运行 in O(n) where n is 50 below, 运行s really slowly:

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 交互 运行time 正在缓存结果,那么每次调用 lucas 应该只涉及前两个缓存项的总和。一次加法运算找到第 3 项,一次额外的加法运算找到第 4 项,一次额外的加法运算找到第 5 项,因此考虑到缓存,总共只有 48 次加法就达到了第 50 项。该函数应该不会花费接近一秒的时间来找到至少前几千个词,为什么性能如此糟糕?

为了lucas 500计算,我已经等了半个多小时了。

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

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

使用list-lazyness

这是一个简单的版本,与您的 code-snippet 功能相同,但速度应该更快 - 它会将已经计算的部分保留在列表本身中:

lucasNumbers :: [Integer]
lucasNumbers = 1:3:zipWith (+) lucasNumbers (tail lucasNumbers)

first50 :: [Integer]
first50 = take 50 lucasNumbers

之所以更快,是因为现在列表的惰性将帮助您记住不同的部分

如果您查找 Fibonacci sequences in Haskell(它实际上与您的一样;))

,您可以了解很多相关信息

使用unfoldr

另一种(可能看起来 魔法 更少)的方法是使用 Data.List.unfoldr - 这里已经计算的部分(或那些重要的部分 - 最后一个和 second-to-last 个元素)将处于 状态 你传递给 展开 操作:

lucasNumbers :: [Integer]
lucasNumbers = unfoldr (\ (n,n') -> Just (n, (n',n+n'))) (1,3)

给你的 comment/question:

假设你在谈论 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 版本