1. 程式人生 > >【題10 斐波那契數列】

【題10 斐波那契數列】

演算法和資料操作
遞迴和迴圈:
很多演算法可以用遞迴和迴圈兩種不同方式實現。
通常基於遞迴的實現方法程式碼會比較簡潔,但效能不如基於迴圈的實現方法。
排序和查詢:
重點掌握二分查詢,歸併排序和快速排序。
題11旋轉陣列的最小數字
回溯法:
要求在二維陣列(迷宮或棋盤)上搜索路徑用回溯法。
通常回溯法很適合用遞迴的程式碼實現。
如果限定不可以用遞迴,考慮用棧來模擬。
【題12矩陣中的路徑,13 機器人的運動範圍】
動態規劃:
求某個問題的最優解,並且該問題可以分為多個子問題。
用自上而下的遞迴思路去分析動態規劃問題的時候,會發現子問題之間存在重疊的更小的子問題。
為了避免重複計算,用自下而上的迴圈程式碼實現,把子問題的最優解先算出來並用陣列(一維或者二維)儲存下來,接下來基於子問題的解計算大問題的解。
貪婪演算法:


動態規劃的思路後,還在提醒在分解子問題的時候是不是存在某個特殊的選擇,如果採用這個特殊的選擇將一定能得到最優解,那麼採用貪婪演算法。
【題14 剪繩子】
位運算:
一類特殊的演算法,把數字表示成二進位制之後對0和1的操作。由於位運算的物件為二進位制數字,所以不是很直觀,
共有與,或,異或,左移和右移5種運算。
【題15 二進位制中1的個數】

遞迴和迴圈
遞迴是在一個函式的內部呼叫這個函式自身。
迴圈是通過設定計算的初始值和終止條件,在一個範圍內重複計算。
遞迴優點:簡潔
遞迴缺點:
(1)由於是函式呼叫自身,函式呼叫是由時間和空間消耗的。
(2)很多計算時重複的,對效能帶來負面影響。
(3)棧溢位:函式呼叫在棧中分配空間,而每個程序的棧的容量時有限的,當遞迴呼叫的層級太多時,就會超出棧的容量,導致棧溢位。
【題10 斐波那契數列】


【題目一】
寫一個函式,輸入n,求斐波那契(Fibonacci)數列的第n項,斐波那契數列的定義如下
在這裡插入圖片描述
<效率很低的解法,不喜歡>
在這裡插入圖片描述
評價:
這種解法存在效率問題。這棵樹中有很多節點是重複的,而且重複的節點會隨著n的增大而急劇增加,這意味著計算量會隨著n的增大而急劇增大。用遞迴方法計算的時間複雜度是以n的指數的方式遞增的。

<期待的實用解法:避免重複計算>
從下往上計算
(1) 首先根據f(0)和f(1)計算出f(2),
(2) 再根據f(1)和f(2)計算f(3)
……
依次類推算出第n項了。時間複雜度為O(n)

<時間複雜度O(logn),但不夠實用的解法>


數學公式:(可以用數學歸納法證明)
在這裡插入圖片描述
有了這個公式,只需求得矩陣
在這裡插入圖片描述
即得到f(n)
現在問題轉成如何求矩陣
在這裡插入圖片描述
的乘方。
考慮乘方的性質。
在這裡插入圖片描述
想求n次方,就要先求得n/2次方,再把n/2次方的結果平方一下即可。遞迴實現。

實現

package ti10;

/**
 * 劍指offer面試題9:斐波那契數列
 * 題目:寫一個函式,輸入n,求斐波那契數列的第n項。
 *                     0,                n=1
 *     斐波那契數列定義如下:f(n)=      1,                n=2
 *                                 f(n-1)+f(n-2),    n>2

 */
public class No9Fibonacci {

    public static void main(String[] args) {
        System.out.println("第4項斐波那契數列的值為:"+fibonacci(4));
    }

    /*
     * 採用遞迴實現斐波那契數列生成函式,效率低
     */
    public static int generateFibonacci(int n){
        if(n==0)
            return 0;
        if(n==1)
            return 1;
        return generateFibonacci(n-1)+generateFibonacci(n-2);
    }
    
    /*
     * 採用迴圈實現斐波那契數列
     * 儲存數列中間項,求得結果
     */
    public static int fibonacci(int n){
        int[] result={0,1};
        if(n<2)
            return result[n];
        int fibNMinusOne=1;
        int fibNMinusTwo=0;
        int fibN=0;
        for(int i=2;i<=n;i++){
            fibN=fibNMinusOne+fibNMinusTwo;
            fibNMinusTwo=fibNMinusOne;
            fibNMinusOne=fibN;
        }
        return fibN;
    }
}

解法比較:
法一:基於遞迴的解法:直觀,但時間效率很低。實際軟體開發中不會用這種方法。
法二:把遞迴演算法用迴圈實現:極大提高了時間效率。
法三:把求斐波那契數列轉換成求矩陣的乘方。可以用O(logn)求得矩陣的n次方,但由於隱含時間常數較大,很少有軟體採用這種方法。

【題目二:青蛙跳臺階問題】
一隻青蛙依次可以跳上1級臺階,也可以跳上2級臺階。求該青蛙跳上一個n級臺階總共有多少種跳法。
分析:
(1)考慮最簡單情況:
如果只有1級臺階,顯然一種 跳法
如果只有2級臺階,有兩種跳法1.分兩次跳,每次1級。 2.一次兩級
(2)一般情況:
把 n級臺階時跳法看成n的函式,記為f(n).
當n>2時,第一次跳的時候有兩種不同選擇:
1.第一次只跳1級,此時跳法數目等於後面剩下的n-1級臺階的跳法數目f(n-1)
2.第一次跳2級,此時跳法數目等於後面剩下的n-2級臺階跳法數目,即為f(n-2)
因此,n級臺階的不同跳法的總數為f(n)=f(n-1)+f(n-2)

實現

package ti10;

public class P77_FrogJumpFloor {
    public int FrogJumpFloor(int target) {
        int result = 0;
        if (target == 2 || target == 1) {
             result = target;
        }
        int temp1 = 1;
        int temp2 = 2;

        for (int i = 3; i <= target; i++) {
            result = temp1 + temp2;
            temp1 = temp2;
            temp2 = result;
        }
        return result;
    }
    public static void main(String[] args) {
        int n = 2;
        P77_FrogJumpFloor test = new P77_FrogJumpFloor();
        int result = test.FrogJumpFloor(n);
        System.out.print(result);
    }
}

【擴充套件】
在青蛙跳臺階問題中,如果把條件改成:一隻青蛙一次可以跳上1級臺階,也可以跳上2級……它也可以跳上n級,此時該青蛙跳上一個n級臺階總共有多少種跳法?
數學歸納法可以證明f(n)=2^(n-1)
分析
假設f(n)是n個臺階跳的次數。

  1. f(1) = 1
  2. f(2) 會有兩個跳得方式,一次1階或者2階,這回歸到了問題f(1),f(2) = f(2-1) + f(2-2)
  3. f(3) 會有三種跳得方式,1階、2階、3階,那麼就是第一次跳出1階後面剩下:f(3-1);第一次跳出2階,剩下f(3-2);第一次3階,那麼剩下f(3-3).因此結論是f(3) 會有三種跳得方式,1階、2階、3階,那麼就是第一次跳出1階後面剩下:f(3-1);第一次跳出2階,剩下f(3-2);第一次3階,那麼剩下f(3-3).因此結論是
    f(3) = f(3-1)+f(3-2)+f(3-3)
  4. f(n)時,會有n中跳的方式,1階、2階…n階
  5. 得出結論:
    f(n) = f(n-1)+f(n-2)+…+f(n-(n-1)) + f(n-n)
    f(0) + f(1) + f(2) + f(3) + … + f(n-1) == f(n) = 2f(n-1)
    即f(n) = 2
    f(n-1)

每個臺階都有跳與不跳兩種情況(除了最後一個臺階),最後一個臺階必須跳。所以共用2^(n-1)中情況

實現

package ti10;

public class Solution {
    public int JumpFloorII(int target) {
        int result=0;
        if(target==0)
        {result=0;
        }else if(target==1)
        {result=1;
        }else{
            result=2*JumpFloorII(target-1);
        }
        
      return result; 
     
    }
    public static void main(String[] args) {
        int n = 5;
        Solution test = new Solution();
        int result = test.JumpFloorII(n);
        System.out.print(result);
    }

}

【相關題目】
我們可以用21(左圖)的小矩形橫著或者豎著去覆蓋更大的矩形,請問用8個21的小矩形無重疊地覆蓋一個2*8的大矩形(右圖)總共有多少種方法?
在這裡插入圖片描述

把2 * 8的覆蓋方法即為f(8)
第一個2 * 1的小矩形去覆蓋大矩形最左邊時有兩種選擇:豎著放,橫著放。
豎著放時,右邊剩下2 * 7區域,覆蓋方法記為f(7)
橫著放時,當小矩形橫著放在左上角時,左下角必須放一個,
右邊還剩2 * 6,覆蓋方法記為f(6)
因此f(8)=f(7)+f(6).

參考:
1.《劍指offer》
2.https://www.cnblogs.com/gl-developer/p/6445445.html
3.https://blog.csdn.net/Sunshine_liang1/article/details/82468306
4.https://blog.csdn.net/xiaomei920528/article/details/74178927