1. 程式人生 > 其它 >【演算法】查詢演算法

【演算法】查詢演算法

一、查詢演算法介紹  

  • 順序(線性)查詢

  • 二分查詢/折半查詢

  • 插值查詢

  • 斐波那契查詢

二、線性查詢

  • 編寫線性查詢演算法程式碼
 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 }