斐波那契數與二分法的遞歸與非遞歸算法及其復雜度分析


1. 什么是斐波那契數?

這里我借用百度百科上的解釋:斐波那契數,亦稱之為斐波那契數列(意大利語: Successione di Fibonacci),又稱黃金分割數列、費波那西數列、費波拿契數、費氏數列,指的是這樣一個數列:0、1、1、2、3、5、8、13、21、……在數學上,斐波納契數列以如下被以遞歸的方法定義:F0=0,F1=1,Fn=Fn-1+Fn-2(n>=2,n∈N*),用文字來說,就是斐波那契數列列由 0 和 1 開始,之后的斐波那契數列系數就由之前的兩數相加。特別指出:0不是第一項,而是第零項。

在這個斐波那契數列中的數字,就被稱為斐波那契數。這個級數與大自然植物的關系極為密切。幾乎所有花朵的花瓣數都來自這個級數中的一項數字:菠蘿表皮方塊形鱗苞形成兩組旋向相反的螺線,它們的條數必須是這個級數中緊鄰的兩個數字(如左旋8行,右旋13行);還有向日葵花盤……直到最近的1993年,人們才對這個古老而重要的級數給出真正滿意的解釋:此級數中任何相鄰的兩個數,次第相除,其比率都最為接近0.618034……這個值,它的極限就是所謂的"黃金分割數"。

2. 求第N個斐波那契數

求第N個斐波那契數比較簡單可以直接套用公式n = 0,1 時,fib(n) = 1;n > =2 時,fib(n) = fib(n-2) + fib(n-1)在計算時有兩種算法:遞歸和非遞歸。如下:

 1 //非遞歸算法
 2 long long fib1(size_t N) {
 3     long long a = 0, b = 1, c = 0;
 4     if (N < 2) {
 5         return N;
 6     }
 7     else {
 8         for (long long i = 2; i <= N; ++i) {
 9             c = a + b;
10             a = b;
11             b = c;
12         }
13     }
14         return c;
15 }
16 int main()
17 {
18     printf("%lld", fib1(10));
19     getchar();
20     return 0;
21 }  //此算法最大的優點是不存在重復計算,故效率比遞歸算法快的多得多。
 1 //遞歸算法
 2 long long fib2(size_t N) {
 3     if (N < 2) 
 4         return N;
 5     return fib2(N - 1) + fib2(N - 2);
 6 }
 7 int main()
 8 {
 9     printf("%lld", fib2(10));
10     getchar();
11     return 0;
12 }

遞歸可以使程序看起來比較簡潔,但缺點是效率比較低,並且可能導致棧溢出,當需要計算的數稍大一點,就需要很長的計算時間,因此需要靈活使用遞歸。

3. 二分法查找

3.1 二分查找的非遞歸算法

 1 template<typename T>  
 2 T* BinarySearch(T* array,int number,const T& data)  //data要查找的數,number查找范圍長度,array要查找的數組
 3 {  
 4        assert(number>=0);  
 5        int left = 0;  
 6        int right = number-1;  
 7        while (right >= left)  
 8        {  
 9               int mid = (left&right) + ((left^right)>>1);  
10               if (array[mid] > data)  
11               {  
12                      right = mid - 1;  
13               }  
14               else if (array[mid] < data)  
15               {  
16                      left = mid + 1;  
17               }  
18               else  
19               {  
20                      return (array + mid);  
21               }  
22        }  
23        return NULL;  
24 }  

3.2 二分查找遞歸算法

 1 template<typename T>  
 2 T* BinarySearch(T* left,T* right,const T& data)  
 3 {  
 4        assert(left);  
 5        assert(right);  
 6        if (right >=left)  
 7        {  
 8               T* mid =left+(right-left)/2;  
 9               if (*mid == data)  
10                      return mid;  
11               else  
12                      return *mid > data ? BinarySearch(left, mid - 1, data) : BinarySearch(mid + 1, right, data);  
13        }  
14        else  
15        {  
16               return NULL;  
17        }  
18 }  

4. 時間復雜度與空間復雜度

時間復雜度:一般情況下,算法中基本操作重復執行的次數是問題規模n的某個函數f(n),進而分析f(n)隨n的變化情況並確定T(n)的數量級。這里用"O"來表示數量級,給出算法的時間復雜度。
      T(n)=O(f(n));                 
  它表示隨着問題規模的n的增大,算法的執行時間的增長率和f(n)的增長率相同,這稱作算法的漸進時間復雜度,簡稱時間復雜度。而我們一般討論的是最壞時間復雜度,這樣做的原因是:最壞情況下的時間復雜度是算法在任何輸入實例上運行時間的上界,分析最壞的情況以估算算法指向時間的一個上界。
時間復雜度的分析方法:
(1)時間復雜度就是函數中基本操作所執行的次數;
(2)一般默認的是最壞時間復雜度,即分析最壞情況下所能執行的次數;
(3)忽略掉常數項;
(4)關注運行時間的增長趨勢,關注函數式中增長最快的表達式,忽略系數;
(5)計算時間復雜度是估算隨着n的增長函數執行次數的增長趨勢;
(6)遞歸算法的時間復雜度為: 遞歸總次數 * 每次遞歸中基本操作所執行的次數
    常用的時間復雜度有以下七種,算法時間復雜度依次增加:O(1)常數型、O(log2 n)對數型、O(n)線性型、O(nlog2n)二維型、O(n^2)平方型、O(n^3)立方型、O(2^n)指數型。
空間復雜度:
  算法的空間復雜度並不是計算實際占用的空間,而是計算整個算法的輔助空間單元的個數,與問題的規模沒有關系。算法的空間復雜度S(n)定義為該算法所耗費空間的數量級。
  S(n)=O(f(n))  若算法執行時所需要的輔助空間相對於輸入數據量n而言是一個常數,則稱這個算法的輔助空間為O(1); 
  遞歸算法的空間復雜度:遞歸深度N*每次遞歸所要的輔助空間, 如果每次遞歸所需的輔助空間是常數,則遞歸的空間復雜度是 O(N)。

5. 斐波那契數的時間復雜度與空間復雜度分析

5.1 非遞歸算法時間復雜度分析

 使用非遞歸算法求到第n個斐波那契數,是從第2個數開始,將前兩個數相加求求后一個數,再將后一個數賦值給前一個數,再計算前兩個數相加的結果。依次類推直到第n個數,用n-1個數和n-2個數相加求出結果,這樣的好處是,我們只計算了n-1次就求出了結果,即時間復雜度為O(n);我們以代碼中測試數10為例詳解求第十個數的過程。當N=10時,進入函數首先判斷,然后走下面的分支開始計算

計算了N-1次,得出了結果所以時間復雜度是O(N)。

非遞歸算法空間復雜度分析
此函數內部最多時一共開辟了a, b, c, i四個變量空間復雜度是常數,即為O(1)。

5.2 遞歸算法時間復雜度分析

在遞歸算法中,求解fib2(n),把它推到求解fib2(n-1)和fib2(n-2)。也就是說,為計算fib2(n),必須先計算

fib2(n-1)和fib2(n-2),而計算fib2(n-1)和fib2(n-2),時按照表達式及計算法則,需先計算又必須先計算fib2(n-1),而fib2(n-1)由fib2(n-2)和fib2(n-3)計算得來,而這之中的和fib2(n-2)由fib2(n-3)和fib2(n-4)計算得來......依次類推,表面上看不出有何復雜度,但是仔細分析可知,每一個計算fib2(n)的分支都會衍生出計算直至(1)和fib(0),也就是說每個分支都要自己計算數本身到1的斐波那契數列,這樣就增加了龐大且冗雜的運算量,還是以10 為例詳細計算說明

圖中數字代表第N個斐波那契數,圖中沒有全部將計算步驟畫出來,但是已經足夠說明問題,它的每一步計算都被分成計算前兩個斐波那契數,以此類推。那么這就形成了一顆二叉樹,雖然不是滿二叉樹,但是我們分析的是最壞時間復雜度,而且只要估算出來遞歸次數隨N增長的趨勢即可,故可以近似將它看成滿二叉樹,其中的節點數就是計算的次數,也就是復雜度,由公式:節點數=2^h-1(h為樹的高度)可得O(2^n)。

遞歸的時間復雜度是:  遞歸次數*每次遞歸中執行基本操作的次數,所以時間復雜度是: O(2^N)

遞歸算法空間復雜度分析:

遞歸最深的那一次所耗費的空間足以容納它所有遞歸過程。遞歸產生的棧偵是要銷毀的,所以空間也就釋放了,要返回上一層棧偵繼續計算+號后面的數,所以它所需要的空間不是一直累加起來的,之后產生的棧偵空間都小於遞歸最深的那一次所耗費的空間。

遞歸的深度*每次遞歸所需的輔助空間的個數 ,所以空間復雜度是:O(N)

6. 求二分法的時間復雜度和空間復雜度

 6.1  非遞歸算法分析

分析:
假設最壞情況下,循環X次之后找到,則:2^x=n; x=logn(算法中如果沒寫,log默認底數為2)
循環的基本次數是log2 N,所以: 時間復雜度是O(logN);
由於輔助空間是常數級別的所以:空間復雜度是O(1);

6.2 遞歸算法復雜度分析

假設最壞情況下,循環X次之后找到,則:2^x=n; x=logn(算法中如果沒寫,log默認底數為2)
遞歸的次數和深度都是log2 N,每次所需要的輔助空間都是常數級別的:
時間復雜度:O(log2 N);
空間復雜度:O(log2N )。

7.  擴展-----不用循環法和遞歸法求1+2+3+...+N(思考一種復雜度為O(1)的解法)

 1 class Temp
 2 {
 3 public:
 4     Temp(){
 5         ++N;
 6         Sum += N;
 7     }
 8     static void Reset(){
 9         N = 0;
10         Sum = 0;
11     }
12     static int GetSum(){
13         return Sum;
14     }
15 private:
16     static int N;
17     static int Sum;
18 };
19 int Temp::N = 0;
20 int Temp::Sum = 0;
21 int solution_Sum(int n){
22     Temp::Reset();
23     Temp *a = new Temp[n];
24     delete[]a;
25     a = 0;
26     return Temp::GetSum();
27 }
28 int main(){
29     cout << solution_Sum(100) << endl;
30     getchar();
31     return 0;
32 
33 }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM