1. 程式人生 > 其它 >第 8 章 查詢演算法

第 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
      單獨拎出來,就會拋異常,哎,燒腦殼~~~改天再想
  • 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