Haskell:为什么我对Fibonacci序列的实现效率低下?

时间:2014-12-28 23:25:57

标签: haskell fibonacci memoization

我已经编写了以下斐波那契游戏程序作为学习Haskell的一部分:

fibonacci 0 = [0]       
fibonacci 1 = [0,1]          
fibonacci n = let   
                foo'1 = last (fibonacci (n-1))
                foo'2 = last (fibonacci (n-2))
              in  reverse((foo'1 + foo'2):reverse (fibonacci (n-1)))

该计划有效:

ghci>fibonacci 6
[0,1,1,2,3,5,8]

但是,性能随着n呈指数下降。如果我给它一个参数30,它需要大约一分钟才能运行,而不是在6瞬间运行。似乎懒惰的执行正在烧毁我,而斐波纳契正在为最终列表中的每个元素运行一次。

我做的事情愚蠢还是缺少某些东西?

(我已经摆脱了可能正在做的++思考)

3 个答案:

答案 0 :(得分:5)

正如评论中指出的那样,你的方法有点过于复杂。特别是,您不需要使用递归调用,甚至是reverse函数,以生成Fibonacci序列。

线性时间实现

除了your own answer之外,这里还有一本教科书单行,它使用了memoization:

fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

获得fibs后,编写fib功能非常简单:

fib :: Int -> [Integer]
fib n
    | n < 0     = error "fib: negative argument"
    | otherwise = take (n+1) fibs

fib的这种实现具有复杂度Θ(n),这显然比Θ(exp(n))好得多。

在GHCi中进行测试

λ> :set +s
λ> fib 6
[0,1,1,2,3,5,8]
(0.02 secs, 7282592 bytes)
λ> fib 30
[0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368,75025,121393,196418,317811,514229,832040]
(0.01 secs, 1035344 bytes)

如您所见,fib 30在我的机器上在一分钟之内得到了评估。

进一步阅读

有关如何在Haskell中生成Fibonacci序列的更全面的处理,我建议你参考haskell.org wiki

答案 1 :(得分:1)

以下是使用@ icktoofay指向memoization的问题的答案。答案包括一个快速返回给定斐波那契数的函数,所以我用他们的例子创建了一个原始问题的解决方案 - 创建一个斐波那契数字列表,直到所要求的数字。

此解决方案几乎可以即时运行(该页面还有将“我的方法”称为“天真”的额外好处)

memoized_fib :: Int -> Integer
memoized_fib = (map fib [0 ..] !!)
   where fib 0 = 0
         fib 1 = 1
         fib n = memoized_fib (n-2) + memoized_fib (n-1)

fib 0 = [0]
fib 1 = [0,1]
fib n = reverse ((memoized_fib (n-2) + memoized_fib(n-1)) : reverse (fib (n-1)))

答案 2 :(得分:1)

您不需要添加备忘录到您的功能 - 它已经拥有所有以前的结果,并生成一个列表。您只需要停止忽略这些结果,就像现在使用last一样。

首先,如果以相反的顺序构建列表更自然,那么没有理由不这样做:

revFib 0 = [0]       
revFib 1 = [1,0]          
revFib n | n > 0 = let  f1 = head (revFib (n-1))
                        f2 = head (revFib (n-2))
                   in  f1 + f2 : revFib (n-1)

这仍然很慢,因为我们仍然忽略了所有以前的结果,除了最后一个结果,位于列表的头部。我们可以停止这样做,

revFib 0 = [0]       
revFib 1 = [1,0]          
revFib n | n > 0 = let  f1 = head (revFib (n-1))
                        f2 = head (tail (revFib (n-1)))
                   in  f1 + f2 : revFib (n-1)

然后我们将命名公共子表达式,以便它在其用途之间共享,并且只计算一次:

revFib 0 = [0]       
revFib 1 = [1,0]          
revFib n | n > 0 = let  prevs = revFib (n-1)
                        [f1,f2] = take 2 prevs
                   in  f1 + f2 : prevs

突然之间它是线性的,而不是指数