【问题标题】:What is the fastest way to write Fibonacci function in Scala?在 Scala 中编写斐波那契函数的最快方法是什么?
【发布时间】:2011-11-15 08:22:09
【问题描述】:

我查看了一些 Scala 中斐波那契函数的实现,从 very simple onemore complicated ones

我不完全确定哪一个是最快的。我倾向于使用 memoization 的印象更快,但是我想知道为什么 Scala 没有本机 memoization。

谁能告诉我编写斐波那契函数的最佳、最快(和最简洁)的方法?

【问题讨论】:

标签: scala recursion fibonacci


【解决方案1】:

下面的代码既快速又能够使用高输入索引进行计算。在我的电脑上,它会在不到两秒的时间内返回 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) )
  }

}

【讨论】:

    【解决方案2】:

    使用Stream 的答案(包括接受的答案)非常简短且惯用,但不是最快的。流会记住它们的值(这在迭代解决方案中不是必需的),即使您不保留对流的引用,可能会分配大量内存,然后立即进行垃圾回收。一个不错的选择是使用Iterator:它不会导致内存分配,风格实用,简短易读。

    def fib(n: Int) = Iterator.iterate(BigInt(0), BigInt(1)) { case (a, b) => (b, a+b) }.
                               map(_._1).drop(n).next
    

    【讨论】:

    • 迭代器根本不起作用,它们保持一个内部状态,在调用 next 时会被修改。您的解决方案确实 allocate (a+b) 创建了一个新的 BigInt 实例,并且您删除了前 n 个元素以到达第 n 个 - 即。你没有机会重用,每次你想得到第 n 个时都必须产生 N + 2 个 BigInts。
    • @JedWesley-Smith 如果您将迭代器分配给变量并对其进行处理,那将不起作用,我同意。但是这里的状态没有暴露。非功能性工具可以以功能性方式使用。
    • @JedWesley-Smith 你说得对,分配了 BigInts;在添加这些数字的功能解决方案中无法避免这种情况。但是根据我的测试,Iterator 可以毫无问题地获得非常高的斐波那契数,而使用 Stream 的解决方案会导致内存不足错误,即使没有保留对流开头的引用。也许使用理想的编译器,使用 Stream 和 Iterator 之间没有区别,但实际上看起来 Iterator 在内存消耗和速度方面都更好。
    【解决方案3】:

    这个问题已经得到解答,但希望我的经验对您有所帮助。我在思考 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)
        }
      }
    

    请注意,我将列表版本短路了,因为如果我不这样做,它将永远运行.. 但是.. 谁在乎? ;^) 因为它只是一段探索性的代码。

    【讨论】:

      【解决方案4】:

      这可以工作。计算一个数字需要 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) + " "))
         }
      }
      

      【讨论】:

      • 我不明白为什么这个答案被否决了。这种尾递归方法等效于使用 while 循环并在恰好 n 个函数调用中计算第 n 个斐波那契数。与使用 f(n-1) + f(n-2) 公式的记忆版本相比,此方法调用自身的次数要少得多。此外,通过尾递归,函数堆栈被重用,从而提高了速度。所以,这个简单的尾递归方法应该更快。那么,为什么要投反对票?
      【解决方案5】:

      一个更简单的尾递归解决方案,可以计算较大的 n 值的斐波那契。当n &gt; 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)
          }
      

      【讨论】:

        【解决方案6】:

        对我来说最简单的定义了一个递归内尾函数:

        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 每次都会产生一个新的流并且您不会从记忆中受益。然后你就不必担心创建一些元组需要几纳秒的一次性成本:)
        • 流是否被保留和重用不是定义的问题。 OP 的问题是关于 fastest 斐波那契,因此减少中间对象的创建是相关的。
        • 速度差异非常小,因此与单个函数调用无关。衡量它的唯一方法是对您的 fib 函数的几百万次调用进行基准测试。在这种情况下,def 的版本将比val 慢数百倍(也比 Landei 的解决方案慢)。
        • Stream 会记住生成的值,如果您一遍又一遍地重用 Stream,那么原始价值函数的成本将被摊销。将上述定义为 val 意味着所有计算值都被强引用,并且仅在外部范围为时才被 GC - def 代表重用到另一个地方。就成本而言,这里唯一的相关成本是尾部功能,这个功能稍微高效。
        • 如果我们不想重用流,这不是一个有效的解决方案,因为可能会分配大量内存,然后立即进行垃圾回收。 A 使用Iterator 发布了一个答案,没有这个问题。
        【解决方案7】:

        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 个术语记忆一次),这可以进行适当的内存/速度权衡。对于最快的是没有明确的答案:这取决于您的使用情况和资源。

        【讨论】:

        • 感谢路易吉的回答。您的代码实际上非常类似于 Scala 在 Stream.scala (lampsvn.epfl.ch/trac/scala/browser/scala/trunk/src///library/…) 行:47 到 51 中的 fib 实现。我同意这取决于我对斐波那契数的使用,我完全同意 Landei 的解决方案会更好,如果我只想计算一个一次性的值。非常感谢!
        • 顺便说一句,#:: 运算符是什么意思?我试图在 Scala 库中找到它,但在任何地方都找不到它......
        • @Enrico 如果您在 Scaladoc 上的搜索框下方查看,则有一个字母索引。 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单例。
        • 嗯,是我自己还是 scaladoc 不适用于 # 页面? dl.dropbox.com/u/2774926/… 我理解你对#:: 的解释,但是如果它起作用的话,# 会非常有用......
        【解决方案8】:

        最快的版本是以某种方式偏离通常的加法方案的版本。非常快的计算在某种程度上类似于基于这些公式的快速二进制求幂:

        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
        }
        

        即使这不是很优化(例如,内部循环不是尾递归),它也会击败通常的加法实现。

        【讨论】:

        • +1 我记得在线性代数的练习中推导出了这个公式。这是课程中最有趣的练习。
        • 如果你想走那条路,为什么不使用封闭公式?
        • 因为Binet公式的范围受根的小数精度限制,并且因为计算实数的n次方是否比上面的公式更快还不清楚整数。
        • 我赞成这个解决方案,据我所知,在尽可能简化矩阵的情况下,它确实是总运算量方面最快的解决方案。
        猜你喜欢
        • 1970-01-01
        • 2012-12-29
        • 2014-05-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-22
        • 2020-01-18
        • 1970-01-01
        相关资源
        最近更新 更多