在Scala中错误地实现了Fibonacci序列

时间:2014-06-27 04:59:21

标签: scala fibonacci

参加Scala聚会,我们正在讨论做事的“Scala方式”......

有人向不同的开发人员询问他/她将如何在Scala中实现Fibonacci序列......该人回答了以下代码(只是被告知虽然它有效,但它不是最优的):

def fibonacci(n: Int):BigInt = n match {
    case 0 => 0
    case 1 => 1
    case _ => fibonacci(n - 1) + fibonacci(n - 2)
}
  1. 这种方法有什么问题?

  2. 改进此代码的方法有哪些,Scala方式?

3 个答案:

答案 0 :(得分:2)

该函数的问题如上所述是非尾递归调用。这意味着此处涉及的递归需要堆栈才能工作(在您的示例中,它是调用堆栈)。换句话说,该功能大致相当于:

import scala.collection.mutable.Stack

def fibonacci(n: Int): BigInt = {
  var result = BigInt(0)
  val stack = Stack.empty[Int]
  stack.push(n)

  while (stack.nonEmpty) {
    val x = stack.pop()
    if (x == 1) {
      result += 1
    }
    else if (x > 1) {
      stack.push(x - 2)
      stack.push(x - 1)
    }
  }

  result
}

如您所见,这不是很有效,是吗?在每次迭代中,堆栈的大小增加1,因为您可以查看作为树的调用,这将是一个适当的binary tree,其大小取决于N和数量在它上面留下大约2 N (当N很大时,实际上较少但不变的因素并不重要),所以我们讨论的是O(2 N )时间复杂度和O(n)内存复杂度(即所需的堆栈大小为N)。现在,exponential growth用于所用内存的时间和线性增长。这意味着它需要一个漫长的时间来处理它并且它使用的内存比它应该的多。顺便说一句,作为软件开发人员,Big O notation方面的推理是一个好主意,因为在讨论性能或内存消耗时,首先需要注意这一点。

值得庆幸的是,对于Fibonnaci,我们不需要递归。这是一个更有效的实施方式:

def fibonacci(n: Int): BigInt = {
  var a = BigInt(0)
  var b = BigInt(1)
  var idx = 0

  while (idx < n) {
    val tmp = a
    a = b
    b = tmp + a
    idx += 1
  }

  a
}

这只是一个简单的循环。它不需要堆栈工作。内存复杂度为O(1)(意味着它需要一定量的内存才能工作,与输入无关)。就时间而言,该算法为O(n),大致意味着处理结果涉及N次迭代的循环,因此时间的增长取决于输入N,但是是线性的而不是指数。

在Scala中,您还可以将其描述为tail recursion

import annotation.tailrec 

def fibonacci(n: Int): BigInt = {
  @tailrec
  def loop(a: BigInt, b: BigInt, idx: Int = 0): BigInt = 
    if (idx < n) 
      loop(b, a + b, idx + 1)
    else
      a

  loop(0, 1)
}

这个循环被描述为一个递归函数,但是因为它是一个&#34;尾递归调用&#34;,编译器将这个函数重写为一个简单的循环。您还可以看到@tailrec注释的存在。它不是绝对必要的,编译器会在没有它的情况下将其优化为循环,但是如果你使用这个注释,如果描述的函数不是尾递归,编译器将会出错 - 这很好,因为它&#39 ;当依赖尾递归工作时很容易出错(即你做了改变而没有注意到,bam,函数不再是尾递归)。使用此注释,因为编译器可以保护您。

因此,在这种情况下,您使用不可变的东西(不再指定vars),但它将具有与while循环相同的性能特征。您喜欢哪个版本,这取决于您的偏好。我更喜欢后者,因为我更容易发现不变量和退出条件,而且我喜欢不变性,但其他人更喜欢前者。关于这样做的惯用方法,你也可以使用懒惰的StreamIterable,但FP部门的任何人都不会抱怨尾递归: - )

答案 1 :(得分:1)

这个将不止一次地计算许多子问题。 您可以将算法视为树。

例如,如果您要求fibonacci(4),则需要计算:

fib(4) = fib(3) + fib(2) = 2 + 1 = 3
  // left subtree:
  fib(3) = fib(2) + fib(1) = 1+1 = 2
    // left
    fib(2) = fib(1) + fib(0) = 0+1 = 1
    // right
    fib(1) = 1
  // right
  fib(2) = fib(1) + fib(0) = 0+1 = 1

你可以看到你计算fib(2) 2次,这只会在更高的数字上变得更糟。

至于如何改进此代码:

  • 要么记住值
  • 使用动态编程技术(或多或少的记忆)
  • 或使用更好的算法!

此处有很多关于此主题的问题 - 从这里开始:Efficient calculation of Fibonacci series

或者针对scala特定答案查看此问题:What is the fastest way to write Fibonacci function in Scala?

答案 2 :(得分:-1)

object FibonacciSequence {
   def main(args: Array[String]): Unit = {
      println(fibonacci(10)) 
   }

   def fibonacci(n:Int):Int={
     if(n==0)
       return 0
      if(n==1)
       return 1
      else
       return fibonacci(n-1)+fibonacci(n-2)
   }
}