斐波那契查找不再迷惑

裴波那契查找的來源

裴波那契數列是一串按照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))卻比二分查找差,它還有一個優點就是分割時候只需進行加減運算(二分和插值都有乘/除)

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