【问题标题】:Haskell: Why is my implementation of the Fibonacci sequence inefficient?Haskell:为什么我对斐波那契数列的实现效率低下?
【发布时间】:2014-12-28 23:25:56
【问题描述】:

作为学习 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 时立即运行。看起来懒惰的执行让我很着急,斐波那契为最终列表中的每个元素运行一次。

我是在做傻事还是错过了什么?

(我已经摆脱了可能会这样做的 ++ 想法)

【问题讨论】:

  • 这看起来像是教科书的例子,说明memoization 变得有用。
  • 一个 就足够时,您正在进行 三个 递归调用。我会写reverseFib 以反向返回斐波那契数,因为这样更容易。一旦定义好,您就可以定义fibonacci = reverse . reverseFib,以便在最后只反转一次列表。 (当然,Haskell 中还有其他众所周知的方法,例如在列表而不是函数上递归......但暂时忘记这些)
  • 我认为记忆是这里的关键,因为我每次想引用其中一个成员时都会重新计算列表。我会检查出来的! (哈!我看到递归示例使用了斐波那契数列)。

标签: haskell fibonacci memoization


【解决方案1】:

正如 cmets 中所指出的,您的方法有点过于复杂。特别是,您不需要使用递归调用,甚至不需要使用reverse 函数来生成斐波那契数列。

线性时间实现

除了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 中生成斐波那契数列的更全面的处理方法,请参阅 haskell.org wiki

【讨论】:

  • 这很优雅!哇。
【解决方案2】:

这是使用@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)))

【讨论】:

  • fib n = map memoized_fib [0..n].
  • 叹息...这个旧的 PERL 大脑遇到了范式转换问题。 (不错!)
  • 不是“如何做”,而是“它是什么”……这给了我一个想法:reverse 真的应该被命名为reversed。如果您还没有,还可以从 Data.List 中查看 initstails。 :)
【解决方案3】:

您不需要向您的函数添加 memoization - 它已经拥有所有以前的结果,并生成一个列表。您只需要停止忽略这些结果,就像您现在使用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

突然间它变成了线性而不是指数

【讨论】:

    猜你喜欢
    • 2011-12-12
    • 2011-09-27
    • 2012-12-29
    • 2011-02-17
    • 2012-07-24
    • 1970-01-01
    • 1970-01-01
    • 2017-12-06
    相关资源
    最近更新 更多