【演算法】查詢演算法
一、查詢演算法介紹
-
順序(線性)查詢
-
二分查詢/折半查詢
-
插值查詢
-
斐波那契查詢
二、線性查詢
- 編寫線性查詢演算法程式碼
1 public class SeqSearch { 2 3 public static void main(String[] args) { 4 int[] arr = { 1, 2, 3, 4, 5 };// 沒有順序的陣列 5 int index = seqSearch(arr, -11); 6 if (index == -1) { 7 System.out.println("沒有找到到");8 } else { 9 System.out.println("找到,下標為=" + index); 10 } 11 } 12 13 /** 14 * 這裡我們實現的線性查詢是找到一個滿足條件的值,就返回 15 * 16 * @param arr 17 * @param value 18 * @return 19 */ 20 public static int seqSearch(int[] arr, int value) { 21 // 線性查詢是逐一比對,發現有相同值,就返回下標22 for (int i = 0; i < arr.length; i++) { 23 if (arr[i] == value) { 24 return i; 25 } 26 } 27 return -1; 28 } 29 30 }
三、二分查詢
3.1、二分查詢思路
二分查詢演算法的前提:陣列必須是有序陣列
二分查詢演算法思路分析(遞迴版):
定義兩個輔助指標:left、right ,待查詢的元素在 arr[left]~arr[right] 之間
left 初始值為 0 ,right 初始值為 arr.length - 1
將陣列分成兩半:int mid = (left + right) / 2; ,取陣列中間值與目標值 findVal 比較
如果 mid > findVal ,說明待查詢的值在陣列左半部分
如果 mid < findVal ,說明待查詢的值在陣列右半部分
如果 mid == findVal ,查詢到目標值,返回即可
何時終止遞迴?分為兩種情況:
找到目標值,直接返回目標值 findVal ,結束遞迴即可
未找到目標值:left > right,這樣想:如果遞迴至陣列中只有一個數時(left == right),還沒有找到目標值,繼續執行下一次遞迴時, left 指標和 right 指標總有一個會再走一步,這時 left 和 right 便會錯開,此時 left > right ,返回 -1 並結束遞迴表示沒有找到目標值
3.2、程式碼實現
1 //注意:使用二分查詢的前提是 該陣列是有序的. 2 public class BinarySearch { 3 4 public static void main(String[] args) { 5 6 int arr[] = { 1, 8, 10, 89, 1000, 1234 }; 7 int resIndex = binarySearch(arr, 0, arr.length - 1, 1000); 8 System.out.println("resIndex=" + resIndex); 9 10 } 11 12 // 二分查詢演算法 13 /** 14 * 15 * @param arr 陣列 16 * @param left 左邊的索引 17 * @param right 右邊的索引 18 * @param findVal 要查詢的值 19 * @return 如果找到就返回下標,如果沒有找到,就返回 -1 20 */ 21 public static int binarySearch(int[] arr, int left, int right, int findVal) { 22 23 // 當 left > right 時,說明遞迴整個陣列,但是沒有找到 24 if (left > right) { 25 return -1; 26 } 27 int mid = (left + right) / 2; 28 int midVal = arr[mid]; 29 30 if (findVal > midVal) { // 向 右遞迴 31 return binarySearch(arr, mid + 1, right, findVal); 32 } else if (findVal < midVal) { // 向左遞迴 33 return binarySearch(arr, left, mid - 1, findVal); 34 } else { 35 36 return mid; 37 } 38 39 } 40 41 }
四、插值查詢
4.1、插值查詢基本介紹
插值查詢演算法類似於二分查詢, 不同的是插值查詢每次從自適應 mid 處開始查詢。
4.2、插值查詢圖解
將折半查詢中的求 mid 索引的公式 , low 表示左邊索引 left ,high 表示右邊索引 right ,key 就是前面我們講的 findVal
圖中公式:int mid = low + (high - low) * (key - arr[low]) / (arr[high] - arr[low]) ;
對應前面的程式碼公式:
int mid = left + (right – left) * (findVal – arr[left]) / (arr[right] – arr[left])
4.3、程式碼實現
1 public class InsertValueSearch { 2 3 public static void main(String[] args) { 4 5 int [] arr = new int[100]; 6 for(int i = 0; i < 100; i++) { 7 arr[i] = i + 1; 8 } 9 int index = insertValueSearch(arr, 0, arr.length - 1, 1); 10 System.out.println("index = " + index); 11 12 } 13 14 //編寫插值查詢演算法 15 //說明:插值查詢演算法,也要求陣列是有序的 16 /** 17 * 18 * @param arr 陣列 19 * @param left 左邊索引 20 * @param right 右邊索引 21 * @param findVal 查詢值 22 * @return 如果找到,就返回對應的下標,如果沒有找到,返回-1 23 */ 24 public static int insertValueSearch(int[] arr, int left, int right, int findVal) { 25 26 System.out.println("插值查詢次數~~"); 27 28 //注意:findVal < arr[left] 和 findVal > arr[right] 必須需要,否則我們得到的 mid 可能越界 29 // findVal < arr[left] :說明待查詢的值比陣列中最小的元素都小 30 // findVal > arr[right] :說明待查詢的值比陣列中最大的元素都大 31 if (left > right || findVal < arr[left] || findVal > arr[right]) { 32 return -1; 33 } 34 35 // 求出mid, 自適應,額,這不就是一次函式嗎 36 // findVal = arr[left] 時,mid = left 37 // findVal = arr[right] 時,mid = right 38 int mid = left + (right - left) * (findVal - arr[left]) / (arr[right] - arr[left]); 39 int midVal = arr[mid]; 40 if (findVal > midVal) { // 說明應該向右邊遞迴 41 return insertValueSearch(arr, mid + 1, right, findVal); 42 } else if (findVal < midVal) { // 說明向左遞迴查詢 43 return insertValueSearch(arr, left, mid - 1, findVal); 44 } else { 45 return mid; 46 } 47 48 } 49 }
4.4、總結
- 對於資料量較大,關鍵字分佈比較均勻(最好是線性分佈)的查詢表來說,採用插值查詢,速度較快
- 關鍵字分佈不均勻的情況下, 該方法不一定比折半查詢要好
五、斐波那契查詢
5.1、斐波那契數列
-
黃金分割點是指把一條線段分割為兩部分, 使其中一部分與全長之比等於另一部分與這部分之比。 取其前三位數字的近似值是 0.618。 由於按此比例設計的造型十分美麗, 因此稱為黃金分割, 也稱為中外比。 這是一個神奇的數字, 會帶來意想不到的效果。
-
斐波那契數列 { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 } 發現斐波那契數列的兩個相鄰數的比例, 無限接近 黃金分割值 0.618
5.2、斐波那契查詢介紹
那為什麼一定要等分吶?能不能進行“黃金分割”?也就是 mid = left+0.618(right-left) ,當然mid 要取整數。如果這樣查詢,時間複雜性是多少?也許你還可以程式設計做個試驗,比較一下二分法和“黃金分割”法的執行效率。
斐波那契查詢演算法又稱為黃金分割法查詢演算法,斐波那契查詢原理與前兩種相似, 僅僅改變了中間結點(mid) 的位置,mid 不再是中間或由插值計算得到,而是位於黃金分割點附近, 即 mid = low + F(k-1) - 1
對 F(k)-1 的理解
-
由斐波那契數列 F[k]=F[k-1]+F[k-2] 的性質, 可以得到F[k]-1) =(F[k-1]-1) +(F[k-2]-1) + 1
-
該式說明:只要順序表的長度為 F[k]-1, 則可以將該表分成長度為 F[k-1]-1 和 F[k-2]-1 的兩段 ,即如圖所示。 從而中間位置為 mid=low+F(k-1)-1 ,類似的, 每一子段也可以用相同的方式分割
-
但順序表長度 n 不一定剛好等於 F[k]-1, 所以需要將原來的順序表長度 n 增加至 F[k]-1。 這裡的 k 值只要能使得 F[k]-1 恰好大於或等於 n 即可
-
為什麼陣列總長度是 F(k) - 1 ,而不是 F(k) ?因為湊成 F(k-1) 才能找出中間值,如果陣列長度為 F(k) ,而 F(k) = F(k-1) + F(k-2) ,咋個找中間值嘞?
-
為什麼陣列左邊的長度是 F(k-1) - 1 ,陣列右邊的長度是 F(k-2) - 1 ?就拿個斐波那契數列來說:{ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 } ,54 = 33 + 20 + 1 ,左邊是不是 F(k-1) - 1 ,右邊是不是 F(k-2) - 1 ,也恰好空出了一箇中間值~~~
5.3、斐波那契查詢思路
-
先根據原陣列大小,計算斐波那契數列的得 k 值
-
陣列擴容條件是:增大 k 值(索引從 0 開始),使得陣列長度剛好大於或者等於斐波那契數列中的 F[k]-1 ,我們定義臨時陣列 temp ,temp 後面為 0 的元素都按照陣列最大元素值填充
-
何時終止斐波那契查詢?
-
找到目標值:直接返回目標值索引
-
沒有找到目標值:low 指標和 high 指標相等或者擦肩而過,即 low >= high
-
- 為什麼 low == high 時需要單獨拎出來?
-
low == high 時說明此時陣列中只剩下一個元素(a[low] 或者 a[high])沒有與目標值比較,並且此時 k 有可能等於 0 ,無法執行 mid = low + f[k - 1] - 1; 操作(k - 1 將導致陣列越界)
-
解決辦法:我們在程式的最後,將 a[low] 或者 a[high] 單獨與目標值 value 進行比較即可,我是通過 Debug 解決陣列越界異常的,我並沒有想明白,但是不把 low == high 單獨拎出來,就會拋異常,哎,燒腦殼~~~改天再想
-
-
mid 值怎麼定?mid = low + f[k - 1] - 1 :用黃金分割點確定 mid 的值
-
左右兩條路,你怎麼選?
-
key < temp[mid] :目標值在黃金分割點的左邊,看上面的圖,應該是 k -= 1;
-
key > temp[mid] :目標值在黃金分割點的右邊,看上面的圖,應該是 k -= 2;
-
key = temp[mid] :找到目標值,因為陣列經歷過擴容,後面的值其實有些是多餘的,mid 可能會越界(相對於原陣列來說)
-
mid <= high :證明 mid 索引在原陣列中,返回 mid
-
mid > high 時,證明 mid 索引已經越界(相對於原陣列來說),返回 high
-
-
5.4、程式碼實現
1 public class FibonacciSearch { 2 3 public static int maxSize = 20; 4 5 public static void main(String[] args) { 6 7 int[] arr = { 1, 2, 3, 4, 5 }; 8 System.out.println("index=" + fibSearch(arr, 5)); 9 10 } 11 12 // 因為後面我們mid=low+F(k-1)-1,需要使用到斐波那契數列,因此我們需要先獲取到一個斐波那契數列 13 // 非遞迴方法得到一個斐波那契數列 14 public static int[] fib() { 15 int[] f = new int[maxSize]; 16 f[0] = 1; 17 f[1] = 1; 18 for (int i = 2; i < maxSize; i++) { 19 f[i] = f[i - 1] + f[i - 2]; 20 } 21 return f; 22 } 23 24 // 編寫斐波那契查詢演算法 25 // 使用非遞迴的方式編寫演算法 26 /** 27 * 28 * @param a 陣列 29 * @param key 我們需要查詢的關鍵碼(值) 30 * @return 返回對應的下標,如果沒有-1 31 */ 32 public static int fibSearch(int[] a, int key) { 33 int low = 0; 34 int high = a.length - 1; 35 int k = 0; // 表示斐波那契分割數值的下標 36 int mid = 0; // 存放mid值 37 int f[] = fib(); // 獲取到斐波那契數列 38 // 獲取到斐波那契分割數值的下標 39 while (high > f[k] - 1) { 40 k++; 41 } 42 // 因為 f[k] 值 可能大於 a 的 長度,因此我們需要使用Arrays類,構造一個新的陣列,並指向temp[] 43 // 不足的部分會使用0填充 44 int[] temp = Arrays.copyOf(a, f[k]); 45 // 實際上需求使用a陣列最後的數填充 temp 46 // 舉例: 47 // temp = {1,8, 10, 89, 1000, 1234, 0, 0} => {1,8, 10, 89, 1000, 1234, 1234, 48 // 1234,} 49 for (int i = high + 1; i < temp.length; i++) { 50 temp[i] = a[high]; 51 } 52 53 // 使用while來迴圈處理,找到我們的數 key 54 while (low < high) { // 只要這個條件滿足,就可以找 55 mid = low + f[k - 1] - 1; 56 if (key < temp[mid]) { // 我們應該繼續向陣列的前面查詢(左邊) 57 high = mid - 1; 58 // 為甚是 k-- 59 // 說明 60 // 1. 全部元素 = 前面的元素 + 後邊元素 61 // 2. f[k] = f[k-1] + f[k-2] 62 // 因為 前面有 f[k-1]個元素,所以可以繼續拆分 f[k-1] = f[k-2] + f[k-3] 63 // 即 在 f[k-1] 的前面繼續查詢 k-- 64 // 即下次迴圈 mid = f[k-1-1]-1 65 k--; 66 } else if (key > temp[mid]) { // 我們應該繼續向陣列的後面查詢(右邊) 67 low = mid + 1; 68 // 為什麼是k -=2 69 // 說明 70 // 1. 全部元素 = 前面的元素 + 後邊元素 71 // 2. f[k] = f[k-1] + f[k-2] 72 // 3. 因為後面我們有f[k-2] 所以可以繼續拆分 f[k-1] = f[k-3] + f[k-4] 73 // 4. 即在f[k-2] 的前面進行查詢 k -=2 74 // 5. 即下次迴圈 mid = f[k - 1 - 2] - 1 75 k -= 2; 76 } else { // 找到 77 // 需要確定,返回的是哪個下標 78 if (mid <= high) { 79 return mid; 80 } else { 81 return high; 82 } 83 } 84 } 85 if(a[low]==key) { 86 return low; 87 } 88 else { 89 return -1; 90 } 91 } 92 }