裴波那契查找的來源
裴波那契數列是一串按照F(0)=1,F(1)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)這一條件遞增的一串數字:
1、1、2、3、5、8、13、21 ... ...
兩個相鄰項的比值會逐漸逼近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中長度創建該長度的裴波那契數組,再通過F(0)=1,F(1)=1, F(n)=F(n-1)+F(n-2)生成裴波那契數列為數組賦值
- 以2中的裴波那契數組的最大值為長度創建填充數組,將原待排序數組元素拷貝到填充數組中來, 如果有剩余的未賦值元素, 用原待排序數組的最后一個元素值填充
- 針對填充數組進行關鍵字查找, 查找成功后記得判斷該元素是否來源於后來填充的那部分元素
具體代碼
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))卻比二分查找差,它還有一個優點就是分割時候只需進行加減運算(二分和插值都有乘/除)