1. 程式人生 > >二分查詢(折半查詢)演算法(原理、實現及時間複雜度)

二分查詢(折半查詢)演算法(原理、實現及時間複雜度)

查詢也是有特殊情況的,比如數列本身是有序的。這個有序數列是怎麼產生的呢?有時它可能本身就是有序的,也有可能是我們通過之前所學的排序演算法得到的。

不管怎麼說,我們現在已經得到了有序數列了並需要查詢。這時二分查詢該出場了。

二分查詢(Binary Search)也叫作折半查詢二分查詢有兩個要求,一個是數列有序,另一個是數列使用順序儲存結構(比如陣列)。

二分查詢的原理及實現

二分查詢的實現原理非常簡單,首先要有一個有序的列表。但是如果沒有,則該怎麼辦?可以使用排序演算法進行排序。

以升序數列為例,比較一個元素與數列中的中間位置的元素的大小,如果比中間位置的元素大,則繼續在後半部分的數列中進行二分查詢;如果比中間位置的元素小,則在數列的前半部分進行比較;如果相等,則找到了元素的位置。每次比較的數列長度都會是之前數列的一半,直到找到相等元素的位置或者最終沒有找到要找的元素。

我們先來想象一下,如果數列中有 3 個數,則先與第 2 個數進行比較,如果比第 2 個數大,則與第 2 個數右邊的數列進行二分查詢,這時這個數列就剩下一個數了,直接比較是否相等即可。所以在 3 個數的時候最多比較兩次。

同理,在有 4 個數的時候,我們與中間數進行比較,一般中間數是首加末除以 2 算出來的,這時我們算出來的中間數是 (1+4)/2 等於 2,所以我們把要查詢的數與第 2 個數比較,若比第 2 個數小,則直接與第 1 個數比較;否則與後面兩個數進行二分查詢,這時的中間數是 (3+4)/2 等於 3,也就是後半部分的第 1 個數。再接著進行比較,相等則找到相應的元素,小於則沒有這個數(因為左邊所有的數都已經判斷過了),大於則繼續向右查詢。所以在 4 個數的時候最多比較 3 次。

以此類推,在 5 個數的時候最多查詢 3 次,在 6 個數的時候也是最多查詢 3 次。

下面我們以一個實際的例子來看看二分查詢的操作過程。假設待查詢數列為 1、3、5、7、9、11、19,我們要找的元素為 18,下面進行二分查詢。首先待查數列如圖 1 所示,我們找到中間的元素 7( (1+7)/2=4,第 4 個位置上的元素)。


圖 1 在待查序列中找到中間元素
中間元素為 7,我們要找的元素比 7 大,於是在後半部分查詢,現在後半部分數列為 9、11、19,我們找到中間元素,如圖 2 所示。


圖 2 在待查序列的後半部分找到中間元素
中間元素為 11,與 11 比較,比 11 大,則繼續在後半部分查詢,後半部分只有一個元素 19 了,這時直接與 19 比較,若不相等,則說明在數列中沒有找到元素,結束查詢。

對於這 7 個元素的數列,我們只查詢並比較了 3 次,是不是比較次數很少呢?

下面我們來看看二分查詢的實現。其實我們通過二分查詢的操作步驟,可以很輕易地想出二分查詢使用遞迴實現也很方便。下面我們用遞迴來實現二分查詢。
public class BinarySearch {
    private int[] array;
    /**
     * 遞迴實現二分查詢
     * @param target
     * @return
     */
    public int searchRecursion(int target) {
        if (array != null) {
            return searchRecursion(target, 0, array.length - 1);
        }
        return -1;
    }

    private int searchRecursion(int target, int start, int end) {
        if (start > end) {
            return -1;
        }
        int mid = start + (end - start) / 2;
        if (array[mid] == target) {
            return mid;
        } else if (target < array[mid]) {
            return searchRecursion(target, start, mid - 1);
        } else {
            return searchRecursion(target, mid + 1, end);
        }
    }
}
當然,除了遞迴實現,二分查詢也可以使用非遞迴實現,程式碼如下:
public class BinarySearch {
    private int[] array;

    /**
     * 初始化陣列
     * @param array
     */
    public BinarySearch(int[] array) {
        this.array = array;
    }

    /**
     * 二分查詢
     * @param target
     * @return
     */
    public int search(int target) {
        if (array == null) {
            return -1;
        }

        int start = 0;
        int end = array.length - 1;

        while (start <= end) {
            int mid = start + (end - start) / 2;
            if (array[mid] == target) {
                return mid;
            } else if (target < array[mid]) {
                end = mid - 1;
            } else {
                start = mid + 1;
            }
        }
        return -1;
    }
}
怎麼樣,是不是很簡單?用測試小程式檢查一下吧。
public class BinarySearchTest {
public static void main(String[] args) {
        int[] array = new int[]{1, 3, 5, 7, 9, 11, 19};
        BinarySearch binarySearch = new BinarySearch(array);
        System.out.println(binarySearch.search(0));
        System.out.println(binarySearch.search(11));
        System.out.println(binarySearch.searchRecursion(0));
        System.out.println(binarySearch.searchRecursion(11));
    }
}

二分查詢的優化

這裡我們考慮一下為什麼是二分查詢,而不是三分之一、四分之一查詢。

發散一下思維,在查字典的時候,如果要查以a開頭的單詞,則你會怎麼翻字典?肯定是從最前面開始翻;如果要查以 z 開頭的單詞,則應該會從最後開始翻。顯而易見,你不會採用二分查詢的方式去查這個單詞在哪,因為這樣你會很累。

同樣,假設資料的範圍是 1~10000,讓你找 10,你會怎麼樣?簡單來說,我覺得乾脆用順序查詢好了,因為數列是升序的,沒必要用二分查詢,用順序查詢比二分查詢的比較次數少。

所以經過這樣的考慮,我們可以優化一下二分查詢,並不一定要從正中間開始分,而是儘量找到一個更接近我們要找的那個數字的地方,這樣能夠減少很多查詢次數。

之前我們都是根據長度去找到這個中間位置,現在是根據 key 所在的序列範圍區間去找到這個位置。比如數列是 1~10,待查 key 是 3,我們可能會將大概前面三分之一的地方作為這個劃分點。

不過還是有人給出了更精準的計算方式,即要查詢的位置 P=low+(key-a[low])/(a[high]-a[low])×(high-low),這是有點複雜,但是仔細看一下,這種計算方式其實就是為了找 key 所在的相對位置,讓 key 的值更接近劃分的位置,從而減少比較次數。

這種對二分查詢的優化其實有個名字,叫作插值查詢,插值查詢對於數列比較大並且比較均勻的數列來說,效能會好很多;但是如果數列極不均勻,則插值查詢未必會比二分查詢的效能好。

二分查詢的特點及效能分析

二分查詢的平均查詢長度 ASL 為 ((n+1)log2(n+1))/n-1,有的書上寫的是 log2(n+1)-1,或者是 log2n,具體計算比較麻煩,這裡就不討論了。

二分查詢有個很重要的特點,就是不會查詢數列的全部元素,而查詢的資料量其實正好符合元素的對數,正常情況下每次查詢的元素都在一半一半地減少。所以二分查詢的時間複雜度為 O(log2n) 是毫無疑問的。當然,最好的情況是隻查詢一次就能找到,但是在最壞和一般情況下的確要比順序查詢好了很多。

二分查詢的適用場景

二分查詢要求數列本身有序,所以在選擇的時候需要確認數列是否本身有序,如果無序,則還需要進行排序,確認這樣的代價是否符合實際需求。

其實我們在獲取一個列表的很多時候,可以直接使用資料庫針對某個欄位進行排序,在程式中需要找出某個值的元素時,就很適合使用二分查找了。

二分查詢適合元素稍微多一些的數列,如果元素只有十幾或者幾十個,則其實可以直接使用順序查詢(當然,也有人在順序查詢外面用了一個或幾個大迴圈,執行這幾層大迴圈需要計算機執行百萬、千萬遍,沒有考慮到機器的效能)。

一般對於一個有序列表,如果只需要對其進行一次排序,之後不再變化或者很少變化,則每次進行二分查詢的效率就會很高;但是如果在一個有序列表中頻繁地插入、刪除資料,那麼維護這個有序列表會讓人很累,其實有更好的方案,彆著急,我們慢慢想。