第 8 章 查詢演算法
第 8 章 查詢演算法
1、查詢演算法介紹
- 順序(線性)查詢
- 二分查詢/折半查詢
- 插值查詢
- 斐波那契查詢
2、線性查詢
- 編寫線性查詢演算法程式碼
public class SeqSearch { public static void main(String[] args) { int[] arr = { 1, 2, 3, 4, 5 };// 沒有順序的陣列 int index = seqSearch(arr, -11); if (index == -1) { System.out.println("沒有找到到"); } else { System.out.println("找到,下標為=" + index); } } /** * 這裡我們實現的線性查詢是找到一個滿足條件的值,就返回 * * @param arr * @param value * @return */ public static int seqSearch(int[] arr, int value) { // 線性查詢是逐一比對,發現有相同值,就返回下標 for (int i = 0; i < arr.length; i++) { if (arr[i] == value) { return i; } } return -1; } }
- 程式執行結果
找到,下標為=4
線性查詢,其實就是咱們平常最常用的順序遍歷,找到就返回第一次出現的下標
物件查詢就需要判斷是不是null,仿照arrayList的get方法就明白了,
3、二分查詢
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、程式碼實現
3.2.1、二分查詢(單個值)
- 編寫二分查詢演算法:查詢到目標值就返回
//注意:使用二分查詢的前提是 該陣列是有序的.
public class BinarySearch {
public static void main(String[] args) {
int arr[] = {1, 8, 10, 89, 1000, 1234};
int resIndex = binarySearch(arr, 0, arr.length - 1, 1000);
System.out.println("resIndex=" + resIndex);
}
// 二分查詢演算法
/**
*
* @param arr 陣列
* @param left 左邊的索引
* @param right 右邊的索引
* @param findVal 要查詢的值
* @return 如果找到就返回下標,如果沒有找到,就返回 -1
*/
public static int binarySearch(int[] arr, int left, int right, int findVal) {
// 當 left > right 時,說明遞迴整個陣列,但是沒有找到
if (left > right) {
return -1;
}
int mid = (left + right) / 2;
int midVal = arr[mid];
if (findVal > midVal) { // 向 右遞迴
return binarySearch(arr, mid + 1, right, findVal);
} else if (findVal < midVal) { // 向左遞迴
return binarySearch(arr, left, mid - 1, findVal);
} else {
return mid;
}
}
}
- 程式執行結果
resIndex=4
3.2.2、二分查詢(所有值)
- 編寫二分查詢演算法:查詢到所有目標值,在找到目標值之後,分別往左、往右進行擴散搜尋
//注意:使用二分查詢的前提是 該陣列是有序的.
public class BinarySearch {
public static void main(String[] args) {
int arr[] = {1, 8, 10, 89, 1000, 1000, 1000, 1234};
List<Integer> resIndexList = binarySearch(arr, 0, arr.length - 1, 1000);
System.out.println("resIndexList=" + resIndexList);
}
// 完成一個課後思考題:
/*
* 課後思考題: {1,8, 10, 89, 1000, 1000,1234} 當一個有序陣列中, 有多個相同的數值時,如何將所有的數值都查詢到,比如這裡的
* 1000
*
* 思路分析 1. 在找到mid 索引值,不要馬上返回 2. 向mid 索引值的左邊掃描,將所有滿足 1000, 的元素的下標,加入到集合ArrayList
* 3. 向mid 索引值的右邊掃描,將所有滿足 1000, 的元素的下標,加入到集合ArrayList 4. 將Arraylist返回
*/
public static List<Integer> binarySearch(int[] arr, int left, int right, int findVal) {
// 當 left > right 時,說明遞迴整個陣列,但是沒有找到
if (left > right) {
return new ArrayList<Integer>();
}
int mid = (left + right) / 2;
int midVal = arr[mid];
if (findVal > midVal) { // 向 右遞迴
return binarySearch(arr, mid + 1, right, findVal);
} else if (findVal < midVal) { // 向左遞迴
return binarySearch(arr, left, mid - 1, findVal);
} else {
// 思路分析
// 1. 在找到mid 索引值,不要馬上返回
// 2. 向mid 索引值的左邊掃描,將所有滿足 1000, 的元素的下標,加入到集合ArrayList
// 3. 向mid 索引值的右邊掃描,將所有滿足 1000, 的元素的下標,加入到集合ArrayList
// 4. 將Arraylist返回
List<Integer> resIndexlist = new ArrayList<Integer>();
// 向mid 索引值的左邊掃描,將所有滿足 1000, 的元素的下標,加入到集合ArrayList
int temp = mid - 1;
while (temp >= 0) {
if (arr[temp] == findVal) {
resIndexlist.add(temp);
}
temp--;
}
resIndexlist.add(mid); //
// 向mid 索引值的右邊掃描,將所有滿足 1000, 的元素的下標,加入到集合ArrayList
temp = mid + 1;
while (temp < arr.length) {
if (arr[temp] == findVal) {
resIndexlist.add(temp);
}
temp++;
}
return resIndexlist;
}
}
}
- 程式執行結果
resIndexList=[4, 5, 6]
4、插值查詢
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])
-
大致思路和二分查詢一樣,有如下不同:
-
尋找 mid 公式不同:
int mid = left + (right – left) * (findVal – arr[left]) / (arr[right] – arr[left]);
-
由於公式中出現 findVal ,所以 findVal 的值不能過大或者過小,否則會引起 mid 過大或過小,引起陣列越界問題,
- 新增判斷:findVal < arr[left] 和 findVal > arr[right]
- why?findVal = arr[left] 時,mid = left;findVal = arr[right] 時,mid = right;
-
4.3、程式碼實現
- 編寫插值查詢演算法
public class InsertValueSearch {
public static void main(String[] args) {
int[] arr = new int[100];
for (int i = 0; i < 100; i++) {
arr[i] = i + 1;
}
int index = insertValueSearch(arr, 0, arr.length - 1, 1);
System.out.println("index = " + index);
}
//編寫插值查詢演算法
//說明:插值查詢演算法,也要求陣列是有序的
/**
*
* @param arr 陣列
* @param left 左邊索引
* @param right 右邊索引
* @param findVal 查詢值
* @return 如果找到,就返回對應的下標,如果沒有找到,返回-1
*/
public static int insertValueSearch(int[] arr, int left, int right, int findVal) {
System.out.println("插值查詢次數~~");
//注意:findVal < arr[left] 和 findVal > arr[right] 必須需要,否則我們得到的 mid 可能越界
// findVal < arr[left] :說明待查詢的值比陣列中最小的元素都小
// findVal > arr[right] :說明待查詢的值比陣列中最大的元素都大
if (left > right || findVal < arr[left] || findVal > arr[right]) {
return -1;
}
// 求出mid, 自適應,額,這不就是一次函式嗎
// findVal = arr[left] 時,mid = left
// findVal = arr[right] 時,mid = right
int mid = left + (right - left) * (findVal - arr[left]) / (arr[right] - arr[left]);
int midVal = arr[mid];
if (findVal > midVal) { // 說明應該向右邊遞迴
return insertValueSearch(arr, mid + 1, right, findVal);
} else if (findVal < midVal) { // 說明向左遞迴查詢
return insertValueSearch(arr, left, mid - 1, findVal);
} else {
return mid;
}
}
}
- 程式執行結果
插值查詢次數~~
index = 0
像直角座標系裡面的函式一樣,知道 兩個點的座標,給出第三個點的縱座標k,讓你找第三個點的橫座標mid;
並且 k要在處於中間位置。
(k-y1)/(y2-y1) = (mid-x1)/(x2-x1)==>求mid。
mid = (k-y1)/(y2-y1) * (x2-x1) + x1
4.4、總結
- 對於資料量較大,關鍵字分佈比較均勻(最好是線性分佈)的查詢表來說,採用插值查詢,速度較快
- 關鍵字分佈不均勻的情況下, 該方法不一定比折半查詢要好
5、斐波那契查詢
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
單獨拎出來,就會拋異常,哎,燒腦殼~~~改天再想
- low == high 時說明此時陣列中只剩下一個元素(a[low] 或者 a[high])沒有與目標值比較,並且此時 k 有可能等於 0 ,無法執行 mid = low + f[k - 1] - 1; 操作(k - 1
- 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、程式碼實現
- 編寫斐波那契查詢演算法
public class FibonacciSearch {
public static int maxSize = 20;
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
System.out.println("index=" + fibSearch(arr, 5));
}
// 因為後面我們mid=low+F(k-1)-1,需要使用到斐波那契數列,因此我們需要先獲取到一個斐波那契數列
// 非遞迴方法得到一個斐波那契數列
public static int[] fib() {
int[] f = new int[maxSize];
f[0] = 1;
f[1] = 1;
for (int i = 2; i < maxSize; i++) {
f[i] = f[i - 1] + f[i - 2];
}
return f;
}
// 編寫斐波那契查詢演算法
// 使用非遞迴的方式編寫演算法
/**
*
* @param a 陣列
* @param key 我們需要查詢的關鍵碼(值)
* @return 返回對應的下標,如果沒有-1
*/
public static int fibSearch(int[] a, int key) {
int low = 0;
int high = a.length - 1;
int k = 0; // 表示斐波那契分割數值的下標
int mid = 0; // 存放mid值
int f[] = fib(); // 獲取到斐波那契數列
// 獲取到斐波那契分割數值的下標
while (high > f[k] - 1) {
k++;
}
// 因為 f[k] 值 可能大於 a 的 長度,因此我們需要使用Arrays類,構造一個新的陣列,並指向temp[]
// 不足的部分會使用0填充
int[] temp = Arrays.copyOf(a, f[k]);
// 實際上需求使用a陣列最後的數填充 temp
// 舉例:
// temp = {1,8, 10, 89, 1000, 1234, 0, 0} => {1,8, 10, 89, 1000, 1234, 1234,
// 1234,}
for (int i = high + 1; i < temp.length; i++) {
temp[i] = a[high];
}
// 使用while來迴圈處理,找到我們的數 key
while (low < high) { // 只要這個條件滿足,就可以找
mid = low + f[k - 1] - 1;
if (key < temp[mid]) { // 我們應該繼續向陣列的前面查詢(左邊)
high = mid - 1;
// 為甚是 k--
// 當前:f[k-1] = f[k-2] + f[k-3]
// 往左查詢:f[k-2] = f[k-3] + f[k-4];
// 假設下次k變為X,那下次進入迴圈就是:f[X-1] = f[k-3] + f[k-4]
// 得到f[X-1] = f[k-2] =>X=k-1,而此處的X又是原來的k,所以k--
k--;
} else if (key > temp[mid]) { // 我們應該繼續向陣列的後面查詢(右邊)
low = mid + 1;
//為什麼是k -=2
// 當前:f[k-1] = f[k-2] + f[k-3]
// 往右查詢:f[k-3] = f[k-4] + f[k-5];
// 假設下次k變為X,那下次進入迴圈就是:f[X-1] = f[k-4] + f[k-5];
// 得到f[X-1] = f[k-3] =>X=k-2,而此處的X又是原來的k,所以k-=2
k -= 2;
} else { // 找到
// 需要確定,返回的是哪個下標
if (mid <= high) {
return mid;
} else {
return high;
}
}
}
if (a[low] == key) {
return low;
} else {
return -1;
}
}
}
- 程式執行結果
index=4