为什么这个简单的 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
版本
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
版本