斐波那契查詢不再迷惑

發表於2020-07-10

裴波那契查詢的來源

裴波那契數列是一串按照F(0)=1,F(1)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)這一條件遞增的一串數字:

1123581321 ... ...

兩個相鄰項的比值會逐漸逼近0.618 —— 黃金分割比值。這個非常神奇的數列在物理,化學等各大領域上有相當的作用, 於是大家想: 能不能把它用在查詢演算法上嘞??

 

於是就有了裴波那契查詢演算法,
裴波那契數列最重要的一個性質是每個數都等於前兩個數之和(從第三個數字開始)。
也就是一個長度為f(n)的陣列,它能被分成f(n-1)和f(n-2)這兩半,
而f(n-1)又能被分為f(n-2)和f(n-3)這兩半。。。直到分到1和1為止(f(1)和f(2))。

斐波那契查詢不再迷惑

(注意一個細節: 在分割時,可以選擇將“大塊”的f(n-1)放前面部分,也可以將“小塊”的f(n-2)放前面,我下面的分割都是按照“大塊”在前進行的)

 

這裡我們發現,二分查詢, 插值查詢和裴波那契查詢的基礎其實都是:對陣列進行分割, 只是各自的標準不同: 二分是從陣列的一半分, 插值是按預測的位置分, 而裴波那契是按它數列的數值分。

 

三個陣列以及它們之間的關係。

瞭解裴波那契查詢的演算法實現, 最重要的是理解“三個陣列”之間的關係,它們分別是:

  • 待查詢陣列 (a)
  • 裴波那契陣列(fiboArray)
  • 填充後陣列(filledArray)

裴波那契陣列

要按裴波那契數分割, 我們當然要建立一個容納有裴波那契數的陣列,那麼,怎麼確定這個陣列的長度呢? 或者說, 怎麼確定陣列裡裴波那契數的最大值呢?(最後一個值)

 

答:只要剛好能滿足我們的需要就可以了,裴波那契陣列的長度,取的是大於等於待查詢陣列長度的最小值。原陣列長4則取5,長6則取8,長13取13(1、1、2、3、5、8、13、21 )

 

填充陣列

其次我們要考慮的是: 我們的陣列長度不可能總是滿足裴波那契數的, 例如5、8、13、21等是裴波那契數, 但我們的陣列長度可能是6,7,10這些非裴波那契數, 那這時候怎麼辦呢? 總不能對長度為10的待查詢陣列按照8和13進行第一次分割吧, 所以我們應該按照上面選定的裴波那契陣列的最大值, 建立一個等於該長度的填充陣列, 將待查詢陣列的元素依次拷貝到填充陣列中, 剩下的部分用原待查詢陣列的最大值填滿。

 

我們進行查詢操作的並不是原待排序陣列, 而是對應的填充陣列!

斐波那契查詢不再迷惑

 

查詢到填充的部分元素如何處理?

當我們在填充陣列中查詢成功後,該元素可能來源於在原陣列的基礎上填充的部分元素(上圖黃色9), 返回的下標(10,11,12)顯然是不準確的,而應該返回原陣列的最後一個元素的下標(9) 。

 

所以,解決方法就是: 在填充陣列中查詢成功後, 判斷返回的元素下標和原陣列長度的關係,如果:返回下標 > 原陣列長度 - 1, 那麼改為返回原陣列最後一個元素下標就OK了。

 

查詢過程

OK,有了上面的基礎我們總結下查詢的過程:

  1. 根據待查詢陣列長度確定裴波那契陣列的長度(或最大元素值)
  2. 根據1中長度建立該長度的裴波那契陣列,再通過F(0)=1,F(1)=1, F(n)=F(n-1)+F(n-2)生成裴波那契數列為陣列賦值
  3. 以2中的裴波那契陣列的最大值為長度建立填充陣列,將原待排序陣列元素拷貝到填充陣列中來, 如果有剩餘的未賦值元素, 用原待排序陣列的最後一個元素值填充
  4. 針對填充陣列進行關鍵字查詢, 查詢成功後記得判斷該元素是否來源於後來填充的那部分元素

 

具體程式碼

package find;

import java.util.Scanner;

public class FibonacciSearch {

public static void main(String[] args) {
int[] a={0,1,2,3,4,5,6,7,8,9};
System.out.println("請輸入想要查詢的數值:");
Scanner sc=new Scanner(System.in);
int key=sc.nextInt();
int s=search(a,key);
if(s==-1){
System.out.println("沒有這個資料");
}else{
System.out.println("查到資料下標為"+s);
System.out.println("查到資料為第"+(s+1)+"個數");
}
}
/**
* @param a: 待查詢的陣列
* @description: 建立最大值剛好>=待查詢陣列長度的裴波納契陣列
*/
private static int[] makeFiboArray(int[] a) {
int N = a.length;
int first = 1, sec = 1, third = 2, fbLength = 2;
int higt = a[N - 1];
while (third < N) { // 使得裴波那契數不斷遞增,直到值剛好大於等於原陣列長度為止
third = first + sec; // 根據f(n) = f(n-1)+ f(n-2)計算
first = sec;
sec = third;
fbLength++;// 計算最後得到的裴波那契陣列的長度
}
int[] fb = new int[fbLength]; // 根據上面計算的長度建立一個空陣列
fb[0] = 1; // 第一和一二個數是迭代計算裴波那契數的基礎
fb[1] = 1;
for (int i = 2; i < fbLength; i++) {
fb[i] = fb[i - 1] + fb[i - 2]; // 將計算出的裴波那契數依次放入上面的空陣列中
}
return fb;
}

/**
* @description: 裴波那契查詢
*/
public static int search(int[] a, int key) {
int low, high;
int lastA;
int[] fiboArray = makeFiboArray(a);//// 建立最大值剛好>=待查詢陣列長度的裴波納契陣列
int filledLength = fiboArray[fiboArray.length - 1];//建立填充陣列長度
int[] filledArray = new int[filledLength];// 建立長度等於裴波那契陣列最大值的填充陣列
for (int i = 0; i < a.length; i++) {
filledArray[i] = a[i];// 將原待排序陣列的元素都放入填充陣列中
}
lastA = a[a.length - 1];//// 原待排序陣列的最後一個值
for (int i = a.length; i < filledLength; i++) {
filledArray[i] = lastA;//// 如果填充陣列還有空的元素,用原陣列最後一個元素值填滿
}
low = 0;
high = a.length; // 取得原待排序陣列的長度 (注意是原陣列!)
int mid;
int k = fiboArray.length - 1;
while (low <= high) {
mid = low + fiboArray[k - 1] - 1;
if (key < filledArray[mid]) {
high = mid - 1;//排除右半邊的元素
k = k - 1;//f(k-1)是左半邊的長度
} else if (key > filledArray[mid]) {
low = mid - 1;//排除左半邊的元素
k = k - 2;//f(k-2)是右半邊的長度
} else {
if (mid > high) {//說明取得了填充陣列末尾的重複元素了
return high;
} else {
return mid;//說明沒有取到填充陣列末尾的重複元素
}
}
}
return -1;
}
}

 

點選這裡線上執行程式碼

斐波那契查詢的軌跡

斐波那契查詢不再迷惑

不依賴陣列的斐波那契查詢

我百度“斐波那契查詢”的時候, 一大部分基於陣列實現的程式碼都是建立了一個長度固定為20的斐波那契陣列。

 

而第20個斐波那契數是6765,所以這樣的程式碼只能處理長度小於等於6765的陣列。

 

於是就有了另一種編寫斐波那契陣列的方法: 不依賴陣列的編碼方法

 

請點這裡:

不依賴陣列的斐波那契查詢

 

 

說一下這種方法和我上面介紹的方法的不同點

  • 我上面介紹的版本: 先把斐波那契數算出來,再全部用陣列存起來, 要用的時候直接從陣列裡拿就可以了
  • 這個版本: 不用陣列存, 只算出來需要的最大的斐波那契數, 要用的時候“臨時”計算就可以了

 

 

二分,插值和裴波納契查詢的效能比較

 

二分查詢:

二分查詢的軌跡可以用一顆判定樹來表示,例如:將下面的[20,30,40,50,80,90,100]表示為一顆判定樹, 因為一開始查詢的是位於整個陣列1/2 位置的元素50, 所以將50置於根元素位置, 接下來查詢的是30或90,所以放到50的子節點的位置。

 

 

斐波那契查詢不再迷惑

 

 

結合一個結論:具有n個節點的判定樹的深度為logn2 + 1, 所以二分查詢時候比較次數最多為logn2 + 1,

 

插值查詢

上面也說過了,插值查詢只適用於關鍵字均勻分佈的表,在這種情況下, 它的平均效能比二分查詢好,在關鍵字不是均勻分佈時, 它的效能表現就不足人意了。

 

斐波那契查詢

斐波那契查詢的平均效能比二分查詢好, 但最壞情況下的效能(雖然仍然是O(logn))卻比二分查詢差,它還有一個優點就是分割時候只需進行加減運算(二分和插值都有乘/除)

相關文章