先看斐波那契數列的定義:
斐波那契數列(Fibonacci sequence),又稱黃金分割數列、因數學家列昂納多·斐波那契(Leonardoda Fibonacci)以兔子繁殖為例子而引入,故又稱為“兔子數列”,指的是這樣一個數列:1、1、2、3、5、8、13、21、34、……在數學上,斐波納契數列以如下被以遞歸的方法定義:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)
翻譯成java代碼是:
public int Fibonacci(int n) { if (n == 1 || n == 2) { return 1; } return Fibonacci(n - 1) + Fibonacci(n - 2); }
代碼很簡單,簡單的遞歸,就不解釋了。
但是這個方法在實際執行的過程中有可能出現java.lang.StackOverflowError錯誤,這個錯誤是什么意思呢,jdk官方文檔是這么解釋的:應用程序遞歸太深而發生堆棧溢出時,拋出該錯誤。
JAVA程序運行時,會在內存中划分5片空間進行數據的存儲。分別是:1:寄存器。2:本地方法區。3:方法區。4:棧。5:堆。我們上面說的“堆棧”就是指棧了,java棧中主要存儲基本數據類型的值和對象的引用。那么在上面這個遞歸執行的過程中發生了什么呢?
我們先分析一下這個數列的公式:
F(n)=F(n-1)+F(n-2)
=(F(n-2)+F(n-3))+(F(n-3)+F(n-4))
=F(n-3)+F(n-4)+F(n-4)+F(n-5)+F(n-4)+F(n-5)+F(n-5)+F(n-6)
……
觀察可以發現,這個方法在執行的過程中每多增加一層遞歸需要存儲臨時參數的棧空間就是上一層的兩倍,而在最終遞歸結束前,每一層的方法參數n的值都不會被釋放(出棧),所以棧的深度將會以O(2^n)的空間復雜度越壓越深,最終達到最大深度導致棧溢出。
毫無疑問,對實現一個斐波那契數列來說,這個實現空間復雜度O(2^n)太大了,而實際上每一層方法傳入的參數值n在成功傳到下一層的時候就沒用了,但是卻一直不能出棧,導致了棧空間的浪費。
所以要優化這個算法,我們就只能讓每層方法結束后及時出棧,也就是別用遞歸。不用遞歸用什么呢?很多時候,遞歸的功能可以通過循環來實現,循環內部的代碼可以看作是一個方法的方法體,與遞歸不同的地方是循環代碼沒有返回值,但我們可以通過在循環內部更改外部變量的值來實現類似於返回值的效果。
有公式 F(n)=F(n-1)+F(n-2),且已知F(1)=1和F(2)=1,F(3)=1+1=2,F(4)=F(3)+F(2)=2+1=3,利用這個思路,我們可以得到如下的代碼:
public int Fibonacci(int n) { int l = 1, j = 1, k = 1; for (int i = 3; i <= n; i++) { k = l + j; l = j; j = k; } return n == 0 ? 0 : k; }
解釋一下:l 和 j 一開始分別為F(1)和F(2)的值,循環從n>2時開始(小於等於2直接返回k的初始值),每循環一層,k的值變為前兩項 l 和 j 的和,然后更新 l 和 j 的值供下次循環使用。
改良后的算法最大的特點就是棧深度只有一層,沒有無用的變量值浪費空間,空間復雜度達到了最優。