【发布时间】:2011-11-15 08:22:09
【问题描述】:
我查看了一些 Scala 中斐波那契函数的实现,从 very simple one 到 more complicated ones。
我不完全确定哪一个是最快的。我倾向于使用 memoization 的印象更快,但是我想知道为什么 Scala 没有本机 memoization。
谁能告诉我编写斐波那契函数的最佳、最快(和最简洁)的方法?
【问题讨论】:
-
查看this。
我查看了一些 Scala 中斐波那契函数的实现,从 very simple one 到 more complicated ones。
我不完全确定哪一个是最快的。我倾向于使用 memoization 的印象更快,但是我想知道为什么 Scala 没有本机 memoization。
谁能告诉我编写斐波那契函数的最佳、最快(和最简洁)的方法?
【问题讨论】:
下面的代码既快速又能够使用高输入索引进行计算。在我的电脑上,它会在不到两秒的时间内返回 10^6:th 斐波那契数。该算法采用函数式风格,但不使用列表或流。相反,它基于等式 \phi^n = F_{n-1} + F_n*\phi,因为 \phi 是黄金比例。 (这是“Binet 公式”的一个版本。)使用这种等式的问题是 \phi 是无理数(涉及 5 的平方根),因此如果使用浮点数天真地解释有限精度算术,它将因有限精度算术而发散。然而,由于 \phi^2 = 1 + \phi 很容易用 a + b\phi 形式的数字来实现对 a 和 b 整数的精确计算,这就是下面的算法所做的。 (“power”函数有一些优化,但实际上只是“mult”乘法的迭代。)
type Zphi = (BigInt, BigInt)
val phi = (0, 1): Zphi
val mult: (Zphi, Zphi) => Zphi = {
(z, w) => (z._1*w._1 + z._2*w._2, z._1*w._2 + z._2*w._1 + z._2*w._2)
}
val power: (Zphi, Int) => Zphi = {
case (base, ex) if (ex >= 0) => _power((1, 0), base, ex)
case _ => sys.error("no negative power plz")
}
val _power: (Zphi, Zphi, Int) => Zphi = {
case (t, b, e) if (e == 0) => t
case (t, b, e) if ((e & 1) == 1) => _power(mult(t, b), mult(b, b), e >> 1)
case (t, b, e) => _power(t, mult(b, b), e >> 1)
}
val fib: Int => BigInt = {
case n if (n < 0) => 0
case n => power(phi, n)._2
}
编辑:一种更高效且在某种意义上也更惯用的实现是基于用于数值计算和抽象代数的 Typelevel 的 Spire 库。然后可以以更接近数学论证的方式解释上述代码(我们不需要整个环结构,但我认为包含它是“道德上正确的”)。尝试运行以下代码:
import spire.implicits._
import spire.algebra._
case class S(fst: BigInt, snd: BigInt) {
override def toString = s"$fst + $snd"++"φ"
}
object S {
implicit object SRing extends Ring[S] {
def zero = S(0, 0): S
def one = S(1, 0): S
def plus(z: S, w: S) = S(z.fst + w.fst, z.snd + w.snd): S
def negate(z: S) = S(-z.fst, -z.snd): S
def times(z: S, w: S) = S(z.fst * w.fst + z.snd * w.snd
, z.fst * w.snd + z.snd * w.fst + z.snd * w.snd)
}
}
object Fibo {
val phi = S(0, 1)
val fib: Int => BigInt = n => (phi pow n).snd
def main(arg: Array[String]) {
println( fib(1000000) )
}
}
【讨论】:
使用Stream 的答案(包括接受的答案)非常简短且惯用,但不是最快的。流会记住它们的值(这在迭代解决方案中不是必需的),即使您不保留对流的引用,可能会分配大量内存,然后立即进行垃圾回收。一个不错的选择是使用Iterator:它不会导致内存分配,风格实用,简短易读。
def fib(n: Int) = Iterator.iterate(BigInt(0), BigInt(1)) { case (a, b) => (b, a+b) }.
map(_._1).drop(n).next
【讨论】:
这个问题已经得到解答,但希望我的经验对您有所帮助。我在思考 scala 无限流时遇到了很多麻烦。然后,我看了Paul Agron's presentation,他给出了很好的建议:(1)首先使用基本列表实现您的解决方案,然后如果您要使用参数化类型泛化您的解决方案,请首先创建一个简单类型的解决方案,例如 Int 的。
使用这种方法,我想出了一个非常简单(对我来说,易于理解的解决方案):
def fib(h: Int, n: Int) : Stream[Int] = { h #:: fib(n, h + n) }
var x = fib(0,1)
println (s"results: ${(x take 10).toList}")
为了获得上述解决方案,我首先根据 Paul 的建议创建了基于简单列表的“for-dummy”版本:
def fib(h: Int, n: Int) : List[Int] = {
if (h > 100) {
Nil
} else {
h :: fib(n, h + n)
}
}
请注意,我将列表版本短路了,因为如果我不这样做,它将永远运行.. 但是.. 谁在乎? ;^) 因为它只是一段探索性的代码。
【讨论】:
这可以工作。计算一个数字需要 O(1) 空间 O(n) 时间,但没有缓存。
object Fibonacci {
def fibonacci(i : Int) : Int = {
def h(last : Int, cur: Int, num : Int) : Int = {
if ( num == 0) cur
else h(cur, last + cur, num - 1)
}
if (i < 0) - 1
else if (i == 0 || i == 1) 1
else h(1,2,i - 2)
}
def main(args: Array[String]){
(0 to 10).foreach( (x : Int) => print(fibonacci(x) + " "))
}
}
【讨论】:
一个更简单的尾递归解决方案,可以计算较大的 n 值的斐波那契。当n > 46发生整数溢出时,Int 版本更快但有限制
def tailRecursiveBig(n :Int) : BigInt = {
@tailrec
def aux(n : Int, next :BigInt, acc :BigInt) :BigInt ={
if(n == 0) acc
else aux(n-1, acc + next,next)
}
aux(n,1,0)
}
【讨论】:
对我来说最简单的定义了一个递归内尾函数:
def fib: Stream[Long] = {
def tail(h: Long, n: Long): Stream[Long] = h #:: tail(n, h + n)
tail(0, 1)
}
这不需要为 zip 构建任何 Tuple 对象,并且在语法上易于理解。
【讨论】:
def fib 更改为val fib,因为def 每次都会产生一个新的流并且您不会从记忆中受益。然后你就不必担心创建一些元组需要几纳秒的一次性成本:)
fib 函数的几百万次调用进行基准测试。在这种情况下,def 的版本将比val 慢数百倍(也比 Landei 的解决方案慢)。
Iterator 发布了一个答案,没有这个问题。
Scala 确实有 Streams 形式的记忆。
val fib: Stream[BigInt] = 0 #:: 1 #:: fib.zip(fib.tail).map(p => p._1 + p._2)
scala> fib take 100 mkString " "
res22: String = 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 ...
Stream 是一个LinearSeq,因此如果您正在执行大量fib(42) 类型的调用,您可能希望将其转换为IndexedSeq。
但是,我会质疑您的 fibbonaci 函数的用例是什么。它将在不到 100 个术语中溢出 Long,因此较大的术语对任何事情都没有多大用处。如果速度至关重要,您可以将较小的术语放在表格中并查找它们。所以计算的细节可能并不重要,因为对于较小的术语它们都很快。
如果您真的想知道非常大的术语的结果,那么这取决于您是否只想要一次性值(使用 Landei 的解决方案),或者,如果您要进行足够数量的调用,您可能想要预先计算整个批次。这里的问题是,例如,第 100,000 个元素的长度超过 20,000 位。所以我们说的是千兆字节的 BigInt 值,如果你试图将它们保存在内存中,它们会使你的 JVM 崩溃。您可以牺牲准确性并使事情更易于管理。您可以有一个部分记忆策略(例如,每 100 个术语记忆一次),这可以进行适当的内存/速度权衡。对于最快的是没有明确的答案:这取决于您的使用情况和资源。
【讨论】:
A 的左侧是#,您可以使用它来查找符号方法。 #:: 是ConsWrapper 上的一个方法,它是Stream 对象的一个类型成员。但是有一个隐式转换,所以本质上它是 Stream 上的一个方法。它从现有的 Stream 创建一个新的 Stream,其参数位于其头部(就像 :: for List),并且因为它以 : 结尾是右关联的。示例:0 #:: Stream(1,2,3) 与 Stream(1,2,3).#::(0) 相同,等同于 Stream(0,1,2,3)。
Stream.cons(0, Stream(1,2,3)),这是另一种构造Streams的方式,使用Stream单例。
最快的版本是以某种方式偏离通常的加法方案的版本。非常快的计算在某种程度上类似于基于这些公式的快速二进制求幂:
F(2n-1) = F(n)² + F(n-1)²
F(2n) = (2F(n-1) + F(n))*F(n)
下面是一些使用它的代码:
def fib(n:Int):BigInt = {
def fibs(n:Int):(BigInt,BigInt) = if (n == 1) (1,0) else {
val (a,b) = fibs(n/2)
val p = (2*b+a)*a
val q = a*a + b*b
if(n % 2 == 0) (p,q) else (p+q,p)
}
fibs(n)._1
}
即使这不是很优化(例如,内部循环不是尾递归),它也会击败通常的加法实现。
【讨论】: