1. 程式人生 > 其它 >演算法——二分法查詢

演算法——二分法查詢

二分法查詢演算法是一種在有序陣列中查詢特定元素的搜尋演算法。首先,梳理二分查詢演算法實現原理;其次,提供二分查詢演算法的三種不同實現;最後,分析該演算法的侷限性。

摘要

二分法查詢演算法是一種在有序陣列中查詢特定元素的搜尋演算法。首先,梳理二分查詢演算法實現原理;其次,提供二分查詢演算法的三種不同實現;最後,分析該演算法的侷限性。

前言

  在大學上演算法分析課的時候,老師就說二分查詢演算法是一種效率較高的、適用於資料量較大序列的搜尋演算法,此演算法基於順序儲存結構的線性表,且要求序列中元素按關鍵字有序排列,升序和降序都可以。

  二分法的時間複雜度是O(logn),所以在演算法中,比O(n)更優的時間複雜度幾乎只能是O(logn)的二分法。根據時間複雜度來倒推演算法也是面試中的常用策略:題目中若要求演算法的時間複雜度是O(logn),那麼這個演算法基本上就是非二分法莫屬。故本文淺析其三種實現方法,使大家對它有一個更深刻的認知。

  在分析二分查詢演算法之前,我們先梳理計算機中有哪些資料結構。常見的資料結構有陣列(Array)、堆疊(Stack)、佇列(Queue)、連結串列(Linked List)、樹(Tree)、圖(Graph)、堆(Heap)、散列表(Hash)。

演算法原理

  二分法又可以被稱為二分查詢(binary search)或者折半檢索,其基本思想是對於一個有序陣列,每次都通過與陣列中間元素對比,將問題規模縮小為之前的一半,直到找到目標值。廣義的二分查詢是將問題的規模儘可能的縮小到原有的一半。這裡用到了陣列這種資料結構,它屬於常見資料結構之一。

  二分查詢的原理如下:假設待搜尋序列是按照升序排列的,則

  1. 如果序列為空,那麼就返回-1,並退出演算法;這表示查詢不到目標值。

  2. 如果序列不為空,則將它的中間元素與目標值進行比較,看它們是否相等。

  3. 如果相等,則查詢成功,返回該中間元素的索引並退出。

  4. 如果中間元素大於目標值,那麼就將序列的前半部分作為新的待搜尋序列;這是因為後半部分的所有元素都大於目標值,故可以被排除。

  5. 如果中間元素小於目標值,那麼就將當前序列的後半部分作為新的待搜尋序列;這是因為前半部分的所有元素都小於目標值,故可以被排除。

  6. 對新的待搜尋序列重新執行第1步的工作。

  二分查詢之所以是一種效率較高的檢索方法,是因為在匹配失敗的時候,每次都能排除剩餘元素中一半的元素。因此可能包含目標值的有效區間就收縮得很快,而不像順序查詢那樣,每次僅能排除一個元素。

基於折半檢索尋找一個數

  這是折半檢索演算法最簡單的應用場景,可能也是大家最熟悉的,即在指定搜尋範圍內搜尋一個數,如果存在,返回其索引;否則,返回 -1。下面給出兩種非遞迴方式的折半檢索實現方案:

import java.util.Arrays;

/**
 * 二分法查詢
 *
 * @author Wiener 使用二分法的前提是陣列已經排序
 *
 */
public class Demo {

    public static void main(String[] args) {
        int[] arr = { 3, 1, 8, 2, 9, 100,200, 33, 22, 11, 18, 14, 17, 15, 3 };
        // 使用Arrays.sort()排序
        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr));
        // 返回結果
        //int index = binarySearch(arr, 99);
        int index = binarySearch_2(arr, 11);
        System.out.println("index=" + index);
    }

    /**
     * 二分法查詢一返回下標
     * @param arr 陣列
     * @param key 待匹配的值,看陣列中是否存在
     * @return key 在陣列中的下標,如果是-1,就說明陣列中不存在此元素
     */
    public static int binarySearch(int[] arr, int key) {// 陣列和要查詢的數
        int min = 0; // 最小的下標
        int max = arr.length - 1;// 最大的下標
        int mid = (min + max) / 2;// 中間的下標
        while (arr[mid] != key) { // 用於於縮小查詢範圍
            if (key > arr[mid]) { //比中間數還在
                min = mid + 1;   //使最小的下標=中間下標+1
            } else if (key < arr[mid]) {//比中間數還小
                max = mid - 1;   //使最大的下標=中間下標-1
            }
            if(max<min){
                return -1;
            }
            mid=(min+max)/2; //再次計算中間下標
        }

        return mid;
    }
    /*
     * 二分法查詢一如果返回的下標是-1,就說明沒有
     */
    public static int binarySearch_2(int[] arr, int key) {// 陣列和要查詢的數
        int min = 0; // 最小的下標
        int max = arr.length - 1;// 最大的下標
        int mid = min + (max - min) >> 1;// 中間數下標,等價於(min + max) / 2
        while(min<=max){
            if(key>arr[mid]){
                min=mid+1;
            }else if(key<arr[mid]){
                max=mid-1;
            }else{
                return mid;
            }
            mid=(min+max)/2;
        }
        //沒找到
        return -1;

    }

}

  分析二分查詢演算法的一個技巧是:儘量避免出現 else,而是把所有情況用 else if 寫清楚,這樣可以清晰地展現所有細節。

  溫馨提示,退出迴圈的條件之所以是min<=max,而不是min<max,是因為 max = arr.length - 1

  關於mid如何取值,實際上,mid=(min + max) / 2 這種寫法是有缺陷的。因為如果min和max比較大的話,兩者之和就有可能會溢位。改進的地方是將mid的計算方式寫成min + (max - min) >> 1。因為相比除法運算來說,計算機處理位運算效能提升很多。

二分法的遞迴實現

  實際上二分查詢除了以迴圈遍歷來實現,還可以用遞迴來實現。程式碼如下:

    /**
     * 遞迴思想實現二分查詢
     *
     * @param orderedArray 有序陣列
     * @param low      陣列最小值下標
     * @param high     陣列最大值下標
     * @param key      查詢元素
     * @return 查詢元素不存在返回-1,存在返回對應的陣列下標
     */
    public static int internallyBinarySearch(int orderedArray[], int low, int high, int key) {
        int mid = (high - low) / 2 + low;
        if (orderedArray[mid] == key) {
            return mid;
        }
        if (low >= high) {
            return -1;
        } else if (key > orderedArray[mid]) {
            return internallyBinarySearch(orderedArray, mid + 1, high, key);
        } else {
            return internallyBinarySearch(orderedArray, low, mid - 1, key);
        }
    }

應用場景的侷限性

  
  二分查詢的時間複雜度是 O(logn),查詢資料的效率非常高。不過,並不是什麼情況下都可以用二分查詢,它的應用場景是有很大侷限性的。那什麼情況下適合用二分查詢,什麼情況下不適合呢?

  首先,二分查詢依賴的是順序表結構,簡單點說就是陣列。二分查詢只能用在資料是通過順序表來儲存的資料結構上。如果你的資料是通過
連結串列等其它資料結構儲存的,則無法實現二分查詢。

  其次,二分查詢針對的是有序序列。二分查詢對這一點的要求比較苛刻,資料必須是有序的。如果資料沒有排序,我們需要先排序,排序的時間複雜度最低是 O(nlogn)。所以,我們如果針對的是一組靜態資料,而且沒有頻繁地插入、刪除,那麼可以進行一次排序,多次二分查詢。這樣排序的成本可被均攤,二分查詢的邊際成本就會比較低。

  但是,如果我們的數序列有頻繁的插入和刪除操作,要想用二分查詢,要麼每次插入、刪除操作之後保證資料仍然有序,要麼在每次二分查詢之前都先進行排序。針對這種動態資料集合,無論哪種方法,維護有序的成本都是很高的。

  所以,二分查詢只能用在插入、刪除操作不頻繁,一次排序多次查詢的場景中。針對動態變化的資料集合,二分查詢將不再適用。

  再次,資料量太小不適合二分查詢。資料量很小時,順序遍歷就足夠了,完全沒有必要用二分查詢。只有資料量比較大的時候,二分查詢的優勢才會比較明顯。

  有一個例外。如果資料之間的比較操作非常耗時,不管資料量大小,都推薦使用二分查詢。比如,陣列中儲存的都是長度超過 300 的字串,如此長的兩個字串之間比對大小,就會非常耗時。我們需要儘可能地減少比較次數,而比較次數的減少會大大提高效能,這個時候二分查詢就比順序遍歷更有優勢。

  最後,資料量太大也不適合二分查詢。二分查詢的底層需要依賴陣列這種資料結構,而陣列為了支援隨機訪問的特性,要求記憶體空間連續,對記憶體的要求比較苛刻。比如,我們有 1GB 大小的資料,如果希望用陣列來儲存,那就需要 1GB 的連續記憶體空間,但是我們的記憶體都是離散的,可能電腦沒有這麼多的記憶體。

  注意這裡的“連續”二字,也就是說,即便有 2GB 的記憶體空間剩餘,但是如果這剩餘的 2GB 記憶體空間都是零散的,沒有連續的 1GB 大小的記憶體空間,那照樣無法申請一個 1GB 大小的記憶體空間,那照樣無法申請一個 1GB 大小的陣列。而我們的二分查詢是作用在陣列這種資料結構之上的,所以太大的資料用陣列儲存就比較吃力了,也就不能用二分查找了。

如何在 1000 萬個整數中快速查詢某個整數?

  假設記憶體限制是 100MB,每個資料大小是 8 位元組,最簡單的辦法就是將資料儲存在陣列中,記憶體佔用差不多是 10000000/1024/1024 ≈ 76.3MB,符合記憶體的限制。我們可以先對這 1000 萬資料進行排序,然後再利用二分查詢演算法,就可以快速地查詢想要的資料了。

  歡迎點贊閱讀,一同學習交流;若有疑問,請在文章下方留下你的神評妙論!

Reference

https://zhuanlan.zhihu.com/p/65610304
https://blog.csdn.net/weixin_41923658/article/details/86090645
https://www.cnblogs.com/gshao/p/13489436.html#_label3


  讀後有收穫,小禮物走一走,請作者喝咖啡。

讚賞支援