與功能的問題,如所描述是非尾遞歸調用。這意味着這裏涉及的遞歸需要一個棧來工作(在你的示例中,它是調用棧)。換句話說,該功能大致相當於:
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
}
正如你所看到的那樣,這不是很有效率嗎?在每次迭代中,堆棧的大小都會增加一個,因爲您可以查看樹形調用,這將是一個合適的binary tree,其大小取決於N
以及其上的樹葉數大約爲2 N(實際上較少但是當N很大時常數因子並不重要),所以我們討論的是O(2 N)時間複雜度和O(n)存儲器複雜度(即所需堆棧大小爲N
)。現在這是exponential growth時間和使用的內存線性增長。這意味着需要花費很長時間來處理,而且它使用的內存比應該多。順便說一下,作爲一個軟件開發人員,推薦Big O notation是個不錯的主意,因爲這是您在談論性能或內存消耗時首先需要考慮的事情。
謝天謝地,對於斐波那契我們不需要遞歸。這是一個更高效的實現:
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)
}
這個循環被描述爲一個遞歸函數,但因爲這是一個「尾遞歸調用」,編譯器重寫此功能一簡單的循環。您還可以看到@tailrec
註釋的存在。這不是絕對必要的,編譯器會將它優化成沒有它的循環,但是如果你使用這個註解,如果所描述的函數不是尾遞歸,那麼編譯器會出錯 - 這很好,因爲依賴時很容易出錯在尾遞歸上工作(即,你做了一個改變,並且沒有注意到,bam,函數不再是尾遞歸)。使用這個註釋,因爲編譯器可以保護你,如果你這樣做。
因此,在這種情況下,您使用不可變的東西(不再指定更多變量),但它將具有與while循環相同的性能特徵。您喜歡哪個版本,這取決於您的偏好。我更喜歡後者,因爲我更容易發現不變量和退出條件,再加上我喜歡不變性,但其他人更喜歡前者。關於這種做法的慣用方式,您也可以使用懶惰的Stream
或Iterable
,但FP部門中沒有人會抱怨尾部遞歸:-)
可能的重複項:http://stackoverflow.com/問題/ 7388416 /什麼是最快的方式寫入斐波那契函數在斯卡拉 –
你是說這個問題是一個可能的重複或該方法會產生一個可能的重複? –
至於「這個方法有什麼問題?」,這是一個比Scala問題更普遍的算法問題。問題是它重複**很多工作。 –