我是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))]
我知道,这很尴尬。我找不到时间查找并写出更好的文章。虽然我想知道是什么让这么低效。我知道我应该查阅一下,希望有人觉得需要教学并且不需要我的努力。
答案 0 :(得分:9)
orangegoat's answer和Sec 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的惰性求值来根据需要生成fibs
和tail 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''
(此处c
,c'
和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
功能并不昂贵,我们这里只称它一次。