好久沒上博客園了,之前說好的一周寫一個博客來記錄自己的考研計划也落空了。
忙着復習,好久都沒有打開電腦,計划也都是寫在紙上了。最新開始數據結構的復習才打開了電腦。
開始敲代碼的感覺真好。看來我注定是一個碼農了。以后還是要多敲敲代碼,畢竟是以后吃飯的家伙,三日不練,生疏啊。
不嘮叨了,說說今天要寫的主題——有序表查找。
(ps 這篇博客是查看程傑老師的大話數據結構后,參考網絡上的文章寫成的。優缺點和時間復雜度這段完全抄錄的程傑老師的原話)。
一、定義
就是字面上的意思,在一張有序表中進行查找。有序表是啥?就是數據按照一定順序排好的表,而不是一堆雜亂無章的數據。
有序表查找的基本前提就是數據是有序的。
二、幾種常見的有序表查找方法
1.折半查找
折半查找(Binary Search) 又稱為 二分查找。
折半查找的基本思想是:
將原始數據分為等份的兩部分,比較關鍵字與中間值的大小,如果關鍵字小於中間值,說明關鍵字落在左半部分,將查找范圍縮小為左半部分,繼續折半查找;
如果關鍵字大於中間值,說明關鍵字落在右半部分,將查找分為縮小為右半部分,繼續折半查找。
通過關鍵字與中間值的對比,不斷縮小查找范圍,最終查找數據。原理圖如下。
二分法的關鍵是中間值(也就是分隔)的選取。通過分隔,我們將查找區間縮小,通過不斷縮小查找范圍,來查找數據。
2、插值查找
二分法將空間分隔的方法非常粗糙,就是講區間折半。
考慮這樣一組數據 {0, 1, 3, 4, 5, 7, 9, 10, 12, 13, 14} ,需要查找的數據是10。
觀察這個數組,數組的前后分布存在相對均勻,如果我們使用折半查找,我們共需要查找 4 次,才能查找到數據。這就是不考慮數據分布,粗糙地選取分隔的后果。
為了改進折半查找的缺點,我們重新選取分隔。
經過算法科學家的推到,改進方案如下:
分隔的取值變成上圖第二個公式。如果使用第二個公式,上述的查找只需要 1 次就可以輕松查找到。
3、斐波那契查找
斐波那契查找是利用黃金分隔原理來實現的。它的本質和二分查找、插值查找沒有區別,都是通過設置分隔,不斷將區間縮小,最后查找到關鍵字的。
與之前兩個查找方法相似,斐波那契的不同也是分隔設置的不同,它是通過斐波那契數列來設置的。
斐波那契數列 F = {0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...}
斐波那契查找的一個限制就是,src的數據個數需要是斐波那契數列中的元素之一。
例如src = {0, 1, 16, 24, 35, 47, 59, 62, 73, 88, 99} 。
該數組的個數為11個,11並不是斐波那契數列的元素,13才是,所以需要將數組擴容到13個,即令src[11]=src[12]=99。
斐波那契查找的具體代碼如下
/** * 從有序表中查找數據 * @param key 需要查找的數據 * @param src 有序表 * @return */ public static int fobSearch(int key,int[] src){ int length = src.length; int low = 0; //low high 的初始值分別等於有序表索引的最小值和最大值 int high = length-1; int fobInex = 0; // 我們后面需要用到的 斐波那契數組中的索引值 int middle = 0 ; //二分法的分隔值 //使用斐波那契查找的要求 就是有序表的元素個數必須是斐波那契數組元素的值 while(length > getFobonacci(fobInex)){ fobInex ++; } //如果有序表的元素個數不等於斐波那契數組元素的值,則需要在后面補全 int newCapacity = getFobonacci(fobInex);//新數組的大小 if(length < newCapacity){ src = Arrays.copyOf(src, newCapacity); for(int i=length;i<newCapacity;i++){//將后續的數值補全 src[i] = src[i-1]; } } HelpUtils.printIntArray(src);//打印一下補全的數組 while(low <= high){ middle = low + getFobonacci(fobInex-1)-1; if (key < src[middle]) { //如果當前查找記錄小於當前分隔記錄 high = middle - 1; fobInex = fobInex - 1; } else if (key > src[middle]) { low = middle + 1; fobInex = fobInex - 2; } else{ if (middle < length) { return middle; } else{ return -1; } } } return -1; }
可以看到分隔的選取依賴於斐波那契數列。
三、優缺點和時間復雜度
二分查找、插值查找和斐波那契查找的時間復雜度都是O(logn)。
二分查找的前提條件是需要有序表順序存儲,對於靜態查找表,一次排序后不再變化,這樣的算法已經比較好了。
但是對於需要頻繁執行插入或者刪除操作的數據集來說,維護有序表的排序會帶來不小的工作量,並不適合使用。
插值查找,對於表長較大,而關鍵字分布比較均勻的查找表來說,插值查找算法的平均性能要比折半查找好得多。
反之,如果數據中分布類似{0,1,9999,999999}這樣極端不均勻的數據,用插值查找未必是最合適的選擇。
斐波那契查找,就平均性能而言,要優於二分查找,但是如果是最壞的情況,比如key=0,那么始終在左側長半區在查找,查找的效率要低於折半查找。
比較關鍵的一點是,插值、折半都需要進行比較復雜的乘除法運算,
而斐波那契只需要進行簡單的加減運算,在海量數據的查找過程中,這種細微的差別可能會影響最終的查找效率。
上述三個有序表查找算法的具體代碼都放在我的github上,歡迎查看。