【问题标题】:Haskell inefficient fibonacci implementationHaskell 低效的斐波那契实现
【发布时间】:2011-12-12 20:00:12
【问题描述】:

我是 haskell 的新手,刚刚学习函数式编程的乐趣。但是在实现斐波那契函数时立即遇到了麻烦。请在下面找到代码。

--fibonacci :: Num -> [Num]
fibonacci 1 = [1]
fibonacci 2 = [1,1]
--fibonacci 3 = [2]
--fibonacci n = fibonacci n-1
fibonacci n = fibonacci (n-1) ++ [last(fibonacci (n-1)) + last(fibonacci (n-2))]

相当尴尬,我知道。我找不到时间查找并写一个更好的。虽然我想知道是什么让这个效率如此低下。我知道我应该查一下,只是希望有人会觉得有必要成为教育者,让我不费吹灰之力。

【问题讨论】:

  • 如果您甚至找不到时间查找更好的答案,那么您怎么能有时间从提出更好答案并解释为什么更好的答案中学习呢?
  • 可能与此重复:stackoverflow.com/questions/6562387/…。在我看来,这些问题是相同的,只是这个问题的代码示例不太清楚。
  • @ThomasM.DuBuisson:已经是午夜了,我写这篇文章的时候很困。所以上下文应该是“现在没有时间”。现在正在查找它并前往 irc..
  • @HaskellElephant:看了一下那个。我把这句话说得不好。将编辑此问题、查找答案并进行更新。

标签: haskell


【解决方案1】:

orangegoat's answerSec Oe's answer 包含一个链接,该链接可能是学习如何在 Haskell 中正确编写斐波那契数列的最佳地点,但这里是您的代码效率低下的一些原因(请注意,您的代码与经典naive definition。优雅?当然。高效?天哪,不):

让我们考虑一下调用时会发生什么

fibonacci 5

扩展为

(fibonacci 4) ++ [(last (fibonacci 4)) + (last (fibonacci 3))]

除了将两个列表与++ 连接在一起之外,我们已经可以看到我们效率低下的一个地方是我们计算fibonacci 4 两次(我们称之为@987654329 的两个地方@. 但它变得最糟糕。

上面写着fibonacci 4的所有地方都扩展到

(fibonacci 3) ++ [(last (fibonacci 3)) + (last (fibonacci 2))]

到处都是fibonacci 3,它扩展为

(fibonacci 2) ++ [(last (fibonacci 2)) + (last (fibonacci 1))]

显然,这个幼稚的定义有很多重复的计算,而且只有当 n 变得越来越大(比如 1000)时,它才会变得更糟。 fibonacci 不是列表,它只是返回列表,所以它不会神奇地记住之前计算的结果。

此外,通过使用last,您必须浏览列表以获取其最后一个元素,这增加了此递归定义的问题(请记住,Haskell 中的列表不支持恒定时间随机访问- - 它们不是动态数组,它们是linked lists)。


确实抑制计算的递归定义(来自提到的链接)的一个示例是这样的:
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

这里,fibs实际上是一个列表,我们可以利用Haskell的惰性求值根据需要生成fibstail fibs,而之前的计算仍然存储在fib中。要获得前五个数字,很简单:

take 5 fibs -- [0,1,1,2,3]

(或者,如果您希望序列从 1 开始,您可以将第一个 0 替换为 1)。

【讨论】:

  • 谢谢我从中学到了很多...我懒得在这里问这个问题而不是抬头看。感谢您的耐心等待。
【解决方案2】:

在 Haskell 中实现斐波那契数列的所有方法只需点击链接即可 http://www.haskell.org/haskellwiki/The_Fibonacci_sequence

【讨论】:

    【解决方案3】:

    此实现效率低下,因为它进行了三个递归调用。如果我们将计算fibonacci n 的递归关系写成一个范式(注意,书呆子读者:不是whnf),它看起来像:

    T(1) = c
    T(2) = c'
    T(n) = T(n-1) + T(n-1) + T(n-2) + c''
    

    (这里的cc'c'' 是一些我们不知道的常数。)这是一个较小的重复:

    S(1) = min(c, c')
    S(n) = 2 * S(n-1)
    

    ...但是这种重复有一个很好的简单封闭形式,即S(n) = min(c, c') * 2^(n-1):它是指数的!坏消息。

    我喜欢您实现的总体思路(即一起跟踪序列的倒数第二项和最后一项),但是您通过递归调用 fibonacci 多次失败了,而这完全没有必要。这是一个修复该错误的版本:

    fibonacci 1 = [1]
    fibonacci 2 = [1,1]
    fibonacci n = case fibonacci (n-1) of
        all@(last:secondLast:_) -> (last + secondLast) : all
    

    这个版本应该会更快。作为一种优化,它以相反的顺序生成列表,但这里最重要的优化是只进行一次递归调用,而不是有效地构建列表。

    【讨论】:

      【解决方案4】:

      因此,即使您不知道更有效的方法,您如何改进您的解决方案?

      首先,看一下签名,您似乎不想要一个无限列表,而是一个给定长度的列表。没关系,无限的东西现在对你来说可能太疯狂了。

      第二个观察是您需要在您的版本中经常访问列表的末尾,这很糟糕。所以这里有一个在处理列表时通常很有用的技巧:编写一个向后工作的版本:

      fibRev 0 = []
      fibRev 1 = [1]
      fibRev 2 = [1,1]
      fibRev n = let zs@(x:y:_) = fibRev (n-1) in (x+y) : zs
      

      这是最后一种情况的工作原理:我们得到一个短一个元素的列表,并将其命名为zs。同时,我们匹配(x:y:_) 模式(@ 的这种用法称为as-pattern)。这为我们提供了该列表的前两个元素。要计算序列的下一个值,我们只需添加这些元素。我们只是将总和 (x+y) 放在我们已经得到的列表 zs 前面。

      现在我们有了斐波那契列表,但它是倒数的。没问题,用reverse

      fibonacci :: Int -> [Int]
      fibonacci n = reverse (fibRev n)
      

      reverse 函数并不昂贵,我们只在这里调用它一次。

      【讨论】:

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