資料結構與演算法——查詢演算法-斐波那契(黃金分割法)查詢

天然呆dull發表於2021-09-03

tip:在學習該篇前,建議去搞懂 二分查詢,關於 二分查詢 請看 資料結構與演算法——查詢演算法-二分查詢

基本介紹

斐波那契(黃金分割法)搜尋(Fibonacci search) ,又稱斐波那契查詢,是區間中單峰函式的搜尋技術。

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

黃金分割 點是指把一條 線段 分割為兩部分,使其中一部分與全長之比等於另一部分與這部分之比。取其前三位數字的近視值是 0.618。由於按此比例設計的造型十分美麗,因此稱為 黃金分割,也稱為 中外比。這是一個神奇的數字,會帶來意想不到的效果。

簡單說,兩條線的比例為 1:1.618,比如上圖的頭和身體的比例、鼻子和嘴巴下巴的比例

斐波那契數列 {1, 1, 2, 3, 5, 8, 13, 21, 34, 55 } 發現斐波那契數列的 兩個相鄰數的比例,無限接近 黃金分割值 0.618

簡單說:

3/2=1.5 、5/3=1.667、8/5=1.6、13/8=1.625 這樣看來,他們的比例值是無限接近的 1.618 的
1/2=0.5 、3/5=0.6、5/8=0.625、8/13=0.6125 這樣看,他們的比例值是無限接近  0.618 的

演算法原理

與 二分查詢、插值查詢 類似,也只是改變了 mid 值

要從這個陣列 arr = {1, 8, 10, 1000} 中查詢數值 1000
斐波那契數列:{1, 1, 2, 3, 5, 8, 13, 21, 34, 55 }
1. 在斐波那契數列中找到符合這個陣列長度的數值,如果沒有,則找最相近的一個,但要大於或等於陣列長度的值
   arr.length = 4, 在斐波那契數列中,沒有 4 這個值,最近的一個是數值 5 ,index = 4
   那麼 k = 4(這裡看不懂請看上面的基本介紹)
2. 要將原陣列的長度擴充為這個 k 對應斐波那契數列中的數值 5,多餘出來的 1 個數字用原陣列 arr 的最後一個值填充
   也就是:
      arr = {1, 8, 10, 1000}  擴充成如下
   newArr = {1, 8, 10, 1000,1000}
3. 計算 mid 值

他的公式是 mid = low + F(k-1) -1 注:F代表斐波那契數列

怎麼理解呢?由上述所知,k = 4,

{1,  1,  2,  3,  5, 8, 13, 21, 34, 55 }
                 ↑
             ↑   k
         ↑  k-1
        k-2

斐波那契數列的特性是除了相鄰數的比例是無限接近黃金分割值,還有一個特性是:相鄰的兩個數相加等於後一個數

就如上所述: 2 + 3 = 53 + 5 = 8;那麼其中公式變數解釋如下:

  • mid :就是我們要對比的值索引
  • low:該線段的最左端
  • F(k-1)F(k-2):即F[k]個元素分割為前半部分F[k-1]個元素,後半部分F[k-2]個元素

那麼 原始arr = {1, 8, 10, 1000} 的長度是 4,擴充為斐波那契數列中的值的長度後,新的陣列長度是 5

k = 4
斐波那契數列 = {1,  1,  2,  3,  5, 8, 13, 21, 34, 55 }
擴充後的有序陣列 = {1, 8, 10, 1000,1000}
根據公式 mid = low + F(k-1) -1
當 low = 0 時:mid = 0 + 3 - 1 = 2
當 low = 1 時:mid = 1 + 3 - 1 = 3
當 low = 2 時:mid = 2 + 3 - 1 = 4 
當 low = 3 時:mid = 3 + 3 - 1 = 5 , 此時新陣列長度為 5,最大下標為 4,已經超過該陣列最大個數了,可以認為沒有找到。
上面的驗證是驗證這個公式是否有效,至少不會出現陣列越界的情況

那麼再來推導驗證下改變 k 的值,上面的 k = 4

當 k = 3 ,low = 0 : mid = 0 + 2 -1 = 1
當 k = 2 ,low = 0 : mid = 0 + 1 -1 = 0
當 k = 1 ,low = 0 : mid = 0 + 1 -1 = 0

可以看到移動其中的 k 也是不會導致陣列越界的。

那麼再來看,kk-1k-2 有啥作用

      arr = {1, 8, 10, 1000}  擴充成如下
   newArr = {1, 8, 10, 1000,1000}
   
{1,  1,  2,  3,  5, 8, 13, 21, 34, 55 }
                 ↑
             ↑   k
         ↑  k-1
        k-2
擴充後的陣列長度為 5
                    - - - - -      這一條線的長度為 5 ,也就是 k = 4
                        ↑
 左側長度為F(k-1) = 3         右側長度為F(k-2) = 2 

那麼再來看下面的圖:

斐波那契數列性質 F[k] = F[k-1] + F[k-2]

由以上性質可以得到上圖的組合: F[k]-1 = (F[k -1] -1) + (F[k-2] -1) + 1

上圖的公式 F[k - 1] - 1 + 中間 mid 佔用 1 個位置 + F[k - 2] -1 + 1 位置 就是這個陣列的所有元素個數。

那麼說明:只要順序表的長度為 F[k]-1F[k]-1是擴充後的有序陣列的最後一個數的下標),則可以將該表分成 長度為 F[k-1]-1F[k-1]-1是陣列分成兩部分後的前半部分的最後一個數的下標,這個也是中間值的下標mid) 和 F[k-2]-1 兩段,如上圖所示

那麼中間值則為:mid = low + F[k-1]-1 注意:這裡講的有點亂你們意會一下。

上面說了這麼多,其實也並沒有說明白他的這個求中間值的公式為什麼是這樣,只是得到了最重要的幾個資訊:

  1. 求中間值是通過斐波那契數列來計算的

  2. 原始陣列和斐波那契數列的關係是:

    ①原始陣列的長度必須擴充至斐波那契數列中某一個值的長度

    ②因為可以通過斐波那契數列的性質,將這一個擴充陣列分成黃金分割的兩段

    ③分成兩段後,就可以查詢中間值,而這個中間值則可稱為黃金分割點

    ④查詢該點,是否是所要查詢的值,如果不是,則根據大小,可繼續將某一段繼續分割,查詢他的黃金分割點

  3. k:進行 k 的減少或增加,必然可以根據斐波那契數列的特性,將數列分成兩段

好了,個人理解差不多就只能這樣了,下面看一遍程式碼實現,結合上面所講,你就明白了,只能感嘆數學之美。

程式碼實現

這裡使用非遞迴的方法

public class FibonacciSearchTest {
    @Test
    public void fibTest() {
        int[] arr = {1, 8, 10, 89, 1000, 1234};
        System.out.println("原陣列:" + Arrays.toString(arr));
        int findVal = 1;
        int result = fibSearch(arr, findVal);
        System.out.println("查詢值 " + findVal + ":" + (result == -1 ? "未找到" : "找到值,索引為:" + result));

        findVal = -1;
        result = fibSearch(arr, findVal);
        System.out.println("查詢值 " + findVal + ":" + (result == -1 ? "未找到" : "找到值,索引為:" + result));

        findVal = 8;
        result = fibSearch(arr, findVal);
        System.out.println("查詢值 " + findVal + ":" + (result == -1 ? "未找到" : "找到值,索引為:" + result));

        findVal = 10;
        result = fibSearch(arr, findVal);
        System.out.println("查詢值 " + findVal + ":" + (result == -1 ? "未找到" : "找到值,索引為:" + result));

        findVal = 1000;
        result = fibSearch(arr, findVal);
        System.out.println("查詢值 " + findVal + ":" + (result == -1 ? "未找到" : "找到值,索引為:" + result));

        findVal = 1234;
        result = fibSearch(arr, findVal);
        System.out.println("查詢值 " + findVal + ":" + (result == -1 ? "未找到" : "找到值,索引為:" + result));

        findVal = 12345;
        result = fibSearch(arr, findVal);
        System.out.println("查詢值 " + findVal + ":" + (result == -1 ? "未找到" : "找到值,索引為:" + result));

    }

    public static int max_size = 20;//生成的斐波那契數列大小

    private int fibSearch(int[] arr, int key) {
        // 構建一個斐波那契數列
        int[] f = fib();
        int k = 0;
        int low = 0;
        int high = arr.length - 1;//原始陣列最後一個值的下標
        // 查詢 k,由陣列長度,找到在斐波那契數列中的一個值
        while (high > f[k] - 1) {//因為high是arr.length - 1,所以f[k] - 1
            k++;
        }

        // 構建臨時陣列
        int[] temp = Arrays.copyOf(arr, f[k]);//這個方法,不足的部分會使用0填充
        // 將臨時陣列擴充的值用原始陣列的最後一個值(最大值)填充
        for (int i = high + 1; i < temp.length; i++) {
            temp[i] = arr[high];
        }
        //中間值下標
        int mid = 0;
        //這裡有一步看不懂的話,結合上面的講解來看
        // 當兩邊沒有交叉的時候,就都可以繼續查詢
        while (low <= high) {
            if (k == 0) {
                // 如果 k = 0 的話,就只有一個元素了,mid 則就是這個元素
                mid = low;
            } else {
                mid = low + f[k - 1] - 1;
            }
            // 要查詢的值說明在陣列的左側
            if (key < temp[mid]) {
                high = mid - 1;
                // 1. 全部元素 = 前面的元素 + 後面的元素
                // 2. f[k] = f[k-1] + f[k-2]
                // k -1 , 得到這一段的個數,然後下一次按照這個個數進行黃金分割
                k--;//左邊部分
            } else if (key > temp[mid]) {// 要查詢的值在陣列的右側
                low = mid + 1;
                k -= 2;//右邊部分
            }else {// 找到的話
                if (mid <= high) {
                    return mid;
                }else {
                 // 當 mid 值大於最高點的話
                // 也就是我們後面填充的值,其實他的索引就是最後一個值,也就是 high(原始陣列最後一個值的下標)
                    return high;
                }
            }
        }
        //迴圈結束,沒有找到
        return -1;
    }
    //斐波那契數列的構建方法
    private int[] fib() {
        int[] f = new int[max_size];
        f[0] = 1;
        f[1] = 1;
        for (int i = 2; i < max_size; i++) {
            f[i] = f[i - 1] + f[i - 2];
        }
        return f;
    }
}

測試程式碼輸出

原陣列:[1, 8, 10, 89, 1000, 1234]
查詢值 1:找到值,索引為:0
查詢值 -1:未找到
查詢值 8:找到值,索引為:1
查詢值 10:找到值,索引為:2
查詢值 1000:找到值,索引為:4
查詢值 1234:找到值,索引為:5
查詢值 12345:未找到

相關文章