斐波那契(黃金分割法)查找算法

  • 斐波那契查找算法也叫做黃金分割查找

斐波那契數列

斐波那契數列,該數列公式爲F(K) = F(k-1) + F(k-2),即 1、1、2、3、5、8、13、21……。F(k-1)/f(K)隨着K的遞增,該數越來越接近黃金分割比例,所以該方法也叫黃金分割法。

原理

對於一個數組來說,如果數組長度爲契波納切數列中的某一個數字,那麼我們就可以用黃金分割比例來分割該數組。當然,如果數組長度沒有達到要求,那麼我們可以嘗試它擴大來滿足要求,

其實,該算法的本質也還是二分法,只不過跟插入排序法一樣,也是將目標的mid值改變成其它的,以至於分割的結果不一樣,查找的效果也不一樣。【也就是說,真正需要我們做的是找到這個mid】

那麼具體是怎樣分割的呢?這裏的mid不再是折半或者插值得到,而是位於黃金分割點附近,即mid = low + F(k-1) -1

這裏用圖片直觀理解一下:
在這裏插入圖片描述

對F(k-1)-1的理解:

  • 由斐波那契數列F(k) = F(k-1) + F(k - 2)的性質,可以得到(F[k] - 1) = (F[k - 1] - 1) + (F[k - 2] - 1) + 1 。該式說明:只要順序表的長度爲F[k]-1,則可以將該表分成長度爲F[k-1]-1和F[k-2]-1的兩段,即如上圖所示。從而中間位置爲mid=low+F(k-1)-1

即:斐波那契查找就是在二分查找的基礎上根據斐波那契數列進行分割的。在斐波那契數列找一個等於略大於查找表中元素個數的數F[ n ],將原查找表擴展爲長度爲F[n] (如果要補充元素,則補充重複最後一個元素,直到滿足F[n]個元素),完成後進行斐波那契分割,即F[n]個元素分割爲前半部分F[n-1]個元素,後半部分F[n-2]個元素,找出要查找的元素在那一部分並遞歸,直到找到。

ps:二分查找, 插值查找和裴波那契查找的基礎其實都是:對數組進行分割, 只是各自的標準不同: 二分是從數組的一半分, 插值是按預測的位置分, 而裴波那契是按它數列的數值分。

例子

一個例子

對於斐波那契數列:1、1、2、3、5、8、13、21、34、55、89……(也可以從0開始),前後兩個數字的比值隨着數列的增加,越來越接近黃金比值0.618。

比如這裏的89,把它想象成整個有序表的元素個數,而89是由前面的兩個斐波那契數34和55相加之後的和,也就是說把元素個數爲89的有序表分成由前55個數據元素組成的前半段和由後34個數據元素組成的後半段,那麼前半段元素個數和整個有序表長度的比值就接近黃金比值0.618,假如要查找的元素在前半段,那麼繼續按照斐波那契數列來看,55 = 34 + 21,所以繼續把前半段分成前34個數據元素的前半段和後21個元素的後半段,繼續查找,如此反覆,直到查找成功或失敗,這樣就把斐波那契數列應用到查找算法中了。
在這裏插入圖片描述

斐波那契查找算法(黃金分割查找算法)

例子二

1、目前由一個有序遞增數組,要在這個數組中找元素99
在這裏插入圖片描述

2、創建一個斐波那契數列,根據:
在這裏插入圖片描述
也即是:斐波那契數列要求原始表中記錄的個數爲某個斐波那契數列 -1 ,也就是數組長度應該是 arr.length = Fabonacci(k) - 1.
又因爲數組的長度不一定剛好是Fabonacci(k) - 1。

  • 如果大於等於,直接返回k
  • 如果斐波那契數列 - 1 小於 arr.length, 那麼k++ : 也就是要查找的區間應該比當前k要大
int k = 0;
while(arr.length > Fabonacci(k) - 1){
	k++;
}

// 當Fab(k) - 1剛好等於數組長度或者略大於數組長度時,當前的k就是我們期待的數組長度,因此跳出循環

在這裏插入圖片描述
3、如果原始表的長度就是我們期望的數組長度就什麼也不幹,如果原始數組中的長度小於我們期望的長度F(k), 則將原始數組的長度擴展到F(n):(如果要補充元素,則補充重複最後一個元素,直到滿足F[n]個元素)。

又因爲java中數組的長度是固定的,因此我們創建一個臨時數組,將原始數組複製到臨時數組,並補充元素
在這裏插入圖片描述

int[] temp = Arrays.copy(arr, Fabonacci(k));
for(int i = arr.length; i < temp.length; i++){
	temp[i] = arr[arr.length - 1];
}

3、完成後進行契波納切數分割·,即F(k)的元素分割爲前半部分F(k - 1)個元素,後半部分F(k-2)個元素

  • 沒有開始之前:low = 0, high = arr.length - 1;臨時數組長度爲fab(k)
  • 求出mid = low + fab(k-1) -1
    在這裏插入圖片描述
    在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

當目標值大於中間值時,k = k - 2;
爲什麼是k -= 2:

  • 全部元素 = 前面的元素 + 後面的元素
  • F[k] = F[k - 1] + F[k - 2] 【應該向右邊找】
  • 因爲後面我們又F[k-1] = F[k-3] + F[k - 4]
  • 即在f[k-2] 的前面進行查找 k -=2
  • 即下次循環 mid = f[k - 1 - 2] - 1
  1. 找出要查找的元素在那一部分並遞歸,直到找到。

代碼實現

目標數組必須是有序數組。

public class TreeNode {

    private static int maxSize = 20;
    public static int[] fib() {
        int[] f = new int[maxSize];
        f[0] = 1;
        f[1] = 1;
        for (int i = 2; i < maxSize; i++) {
            f[i] = f[i - 1] + f[i - 2];
        }
        return f;
    }



    // 數組中沒有重複的元素
    /**
     *
     * @param a  數組
     * @param key 我們需要查找的關鍵碼(值)
     * @return 返回對應的下標,如果沒有-1
     */
    public static int fibSearch(int[] a, int key) {
        int low = 0;
        int high = a.length - 1;
        int k = 0; //表示斐波那契分割數值的下標
        int mid = 0; //存放mid值
        int f[] = fib(); //獲取到斐波那契數列
        //獲取到斐波那契分割數值的下標
        while(high > f[k] - 1) {
            k++;
        }
        //因爲 f[k] 值 可能大於 a 的 長度,因此我們需要使用Arrays類,構造一個新的數組,並指向temp[]
        //不足的部分會使用0填充
        int[] temp = Arrays.copyOf(a, f[k]);
        //實際上需求使用a數組最後的數填充 temp
        //舉例:
        //temp = {1,8, 10, 89, 1000, 1234, 0, 0}  => {1,8, 10, 89, 1000, 1234, 1234, 1234,}
        for(int i = high + 1; i < temp.length; i++) {
            temp[i] = a[high];
        }

        // 使用while來循環處理,找到我們的數 key
        while (low <= high) { // 只要這個條件滿足,就可以找
            mid = low + f[k - 1] - 1;

            if(key < temp[mid]) { //我們應該繼續向數組的前面查找(左邊)
                high = mid - 1;
                //爲甚是 k--
                //說明
                //1. 全部元素 = 前面的元素 + 後邊元素
                //2. f[k] = f[k-1] + f[k-2]
                //因爲 前面有 f[k-1]個元素,所以可以繼續拆分 f[k-1] = f[k-2] + f[k-3]
                //即 在 f[k-1] 的前面繼續查找 k--
                //即下次循環 mid = f[k-1-1]-1
                k--;
            } else if ( key > temp[mid]) { // 我們應該繼續向數組的後面查找(右邊)
                low = mid + 1;
                //爲什麼是k -=2
                //說明
                //1. 全部元素 = 前面的元素 + 後邊元素
                //2. f[k] = f[k-1] + f[k-2]
                //3. 因爲後面我們有f[k-2] 所以可以繼續拆分 f[k-1] = f[k-3] + f[k-4]
                //4. 即在f[k-2] 的前面進行查找 k -=2
                //5. 即下次循環 mid = f[k - 1 - 2] - 1
                k -= 2;
            } else { //找到
                //需要確定,返回的是哪個下標
                if(mid <= high) {
                    return mid;
                } else {
                    return high;
                }
            }
        }
        return -1;
    }



    public static void main(String[] args) {
        int arr[] ={1,2, 3, 4, 5, 99 , 100};

        int i =  fibSearch(arr, 99);
        if (i == -1){
            System.out.println("找不到");
        }else{
            System.out.println("索引" +i+ "值" + arr[i]);
        }
    }
}

爲什麼不直接用(hi-lo)*0.618來尋找分割點

爲什麼不直接用(hi-lo) * 0.618來尋找分割點?這個問題我個人的看法是乘法的開銷較大,而且對於查找效果的提高有限。斐波那契數前後項之比 fib(n) / fib(n - 1) 也只是在n比較大的時候才接近黃金比例1.618…,而且區間的長度不一定爲某個斐波那契數減一(也有可能查找的目標是最後一個數嘛),所以斐波那契查找的意義應該是在效果與開銷之間找到一個平衡,使效率儘可能的最大化

斐波那契查找的時間複雜度還是O(log2n):斐波那契查找,就平均性能而言,要優於二分查找,但是如果是最壞的情況,比如key=0,那麼始終在左側長半區在查找,查找的效率要低於折半查找。

算法與數據結構學習(31)-斐波那契(黃金分割法)查找算法

斐波那契查找算法
參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章