Haskell效率低下的斐波纳契实现

时间:2011-10-21 19:00:20

标签: haskell

我是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))]

我知道,这很尴尬。我找不到时间查找并写出更好的文章。虽然我想知道是什么让这么低效。我知道我应该查阅一下,希望有人觉得需要教学并且不需要我的努力。

4 个答案:

答案 0 :(得分:9)

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

让我们考虑一下你致电

时会发生什么
fibonacci 5

扩展为

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

除了将两个列表与++连在一起之外,我们已经看到一个地方我们效率低下的是我们计算fibonacci 4 两次(这两个地方)我们打电话给fibonacci (n-1)。但它变得最糟糕。

它所说的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,而之前的计算仍然存储在fibs中。要获得前五个数字,它就像:

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

(如果希望序列从1开始,可以选择将前0替换为1。)

答案 1 :(得分:5)

在Haskell中实现斐波那契序列的所有方法都只是遵循链接 http://www.haskell.org/haskellwiki/The_Fibonacci_sequence

答案 2 :(得分:4)

这种实现效率很低,因为它会进行三次递归调用。如果我们写一个递归关系来计算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

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

答案 3 :(得分:0)

所以即使你不了解更有效的方法,你怎么能改进你的解决方案?

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

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

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功能并不昂贵,我们这里只称它一次。