Каков самый быстрый способ записи функции Фибоначчи в Scala?

Я просмотрел несколько реализаций функции в Scala, начиная с очень просто до более сложные.

Я не совсем уверен, какой из них самый быстрый. Я склоняюсь к впечатлению, что те, которые используют memoization, быстрее, однако мне интересно, почему у Scala нет родной memoization.

может ли кто-нибудь просветить меня на лучший и самый быстрый (и самый чистый) способ написать Фибоначчи функции?

8 ответов


для меня самый простой определяет рекурсивную функцию внутреннего хвоста:

def fib: Stream[Long] = {
  def tail(h: Long, n: Long): Stream[Long] = h #:: tail(n, h + n)
  tail(0, 1)
}

Это не нужно создавать какие-либо объекты кортежа для zip и легко понять синтаксически.


самые быстрые версии-это те, которые каким-то образом отклоняются от обычной схемы сложения. Очень быстрый расчет как-то похож на быстрое двоичное возведение в степень, основанное на этих формулах:

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
}

хотя это не очень оптимизировано (например, внутренний цикл не является хвостовым рекурсивным), он будет бить обычные аддитивные реализации.


Scala имеет memoization в виде потоков.

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 поэтому вы можете преобразовать его в IndexedSeq Если вы делаете много fib(42) тип звонков.

однако я бы задался вопросом, что ваш вариант использования для функции fibbonaci. Он будет переполняться долго менее чем в 100 терминах, поэтому большие термины не очень полезны ни для чего. Меньшие термины вы можете просто вставить в таблицу и посмотреть их, если скорость имеет первостепенное значение. Итак, детали вычисления, вероятно, не имеют большого значения, так как для меньших терминов они все быстрые.

если вы действительно хотите знать результаты для очень больших терминов, то это зависит от того, хотите ли вы просто одноразовые значения (использовать решение Landei) или, если вы делаете достаточное количество вызовов, вы можете предварительно вычислить весь лот. Проблема здесь в том, что, например, 100 000-й элемент имеет длину более 20 000 цифр. Итак, мы говорим о гигабайтах значений BigInt, которые разрушат вашу JVM, если вы пытаетесь удержать их в памяти. Вы можете пожертвовать точностью и сделать более послушными. У вас может быть стратегия частичной memoization (скажем, memoize каждый 100-й термин), которая делает подходящий компромисс памяти / скорости. Нет четкой ответить за то, что является самым быстрым: это зависит от вашего использования и ресурсов.


Это может сработать. для вычисления числа требуется 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. Версия Int быстрее, но ограничена, когда n > 46 происходит переполнение целого числа

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)
    }

на это уже ответили, но, надеюсь, вы найдете мой опыт полезным. У меня было много проблем в скала бесконечные потоки. Затем я посмотрел презентация пола Агрона где он дал очень хорошие предложения: (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}")

чтобы добраться до вышеуказанного решения, я сначала создал, по совету Павла, версию "для манекена", основанную на простых списках:

  def fib(h: Int, n: Int) : List[Int] = {
    if (h > 100) {
      Nil
    } else {
      h :: fib(n, h + n)
    }
  }

обратите внимание, что я закоротил версию списка, потому что если бы я этого не сделал, он работал бы вечно.. Но.. какая разница? ;^) поскольку это просто исследовательский бит кода.


ответы на вопросы, используя Stream (включая принятый ответ) очень короткие и идиоматические, но они не самые быстрые. Потоки запоминают свои значения (что не обязательно в итеративных решениях), и даже если вы не сохраняете ссылку на поток,много памяти может быть выделено, а затем сразу мусор-собраны. Хорошей альтернативой является использование Iterator: он не вызывает выделения памяти, функциональный стиль, краткий и четкий.

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

приведенный ниже код является быстрым и способен вычислять с высокими входными индексами. На моем компьютере он возвращает 10^6-ое число Фибоначчи менее чем за две секунды. Алгоритм имеет функциональный стиль, но не использует списки или потоки. Скорее, он основан на равенстве \phi^n = F_{n-1} + F_n*\phi, для \phi золотое сечение. (Это версия "формулы Бине".) Проблема с использованием этого равенства заключается в том, что \phi является иррациональным (включая квадратный корень из пяти), поэтому он будет расходиться из - за конечно-прецизионная арифметика, если она интерпретируется наивно с использованием Float-чисел. Однако, поскольку \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
    }

EDIT: реализация, которая более эффективна и в некотором смысле также более идиоматична, основана о библиотеке шпиля Typelevel для числовых вычислений и абстрактной алгебры. Затем можно перефразировать приведенный выше код намного ближе к математическому аргументу (нам не нужна вся кольцевая структура, но я думаю, что это "морально правильно" включить его). Попробуйте выполнить следующий код:

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) )
  }

}