五、二分法查詢

abc_十號發表於2020-06-06

一、二分法查詢介紹

  • 二分查詢針對的是一個有序的資料集合,查詢思想有點類似分治思想。
  • 每次都通過跟區間的中間元素對比,將待查詢的區間縮小為之前的一半,直到找到要查詢的元素,或者區間被縮小為0。
二分法查詢

1.1、二分法查詢的時間複雜度

  • 我們假設資料大小是 n,每次查詢後資料都會縮小為原來的一半,也就是會除以2。最壞情況下,直到查詢區間被縮小為空,才停止。

  • 這是一個等比數列。其中 n/2^k = 1 時, k 的值就是總共縮小的次數。

  • 而每一次縮小操作只涉及兩個資料的大小比較,所以,經過了 k 次區間縮小操作,時間複雜度就是 O(k)。通過 n/2^k = 1,我們可以求得 k= log2n,所以時間複雜度就是 O(logn)

  • O(logn) 這種對數時間複雜度。這是一種極其高效的時間複雜度,有的時候甚至比時間複雜度是常量級 O(1) 的演算法還要高效。

  • 因為 logn 是一個非常“恐怖”的數量級,即便 n 非常非常大,對應的 logn 也很小。

  • 比如 n 等於2的32次方,這個數大約是42億。也就是說,如果我們在 42 億個資料中用二分查詢一個資料,最多需要比較32次。

  • 大 O 標記法表示時間複雜度的時候,會省略掉常數、係數和低階。

  • 對於常量級時間複雜度的演算法來說, O(1) 有可能表示的是一個非常大的常量值,比如 O(1000)、 O(10000)。

  • 所以,常量級時間複雜度的演算法有時候可能還沒有 O(logn) 的演算法執行效率高。

  • 反過來,對數對應的就是指數。指數時間複雜度的演算法在大規模資料面前是無效的。

1.2、二分法的實現

1、非遞迴實現

/**
 * 二分法查詢,非遞迴實現
 *
 * @param arr
 * @param a
 * @return
 */
public static int binarySearch(int[] arr, int a) {
    int start = 0;
    int end = arr.length - 1;

    while (start <= end) { // 不能是start < end
        int mid = start + ((end - start) >> 1); //start + (end - start)/2 位運算子速度快
        if (arr[mid] == a) {
            return mid;
        } else if (arr[mid] > a) {
            end = mid - 1; // end = min 有時會造成死迴圈
        } else {
            start = mid + 1;
        }
    }
    return -1;
}

2、遞迴實現

/**
 * 二分法遞迴實現
 *
 * @param arr
 * @param start
 * @param end
 * @param a
 * @return
 */
public static int binarySearch2(int[] arr, int start, int end, int a) {
    if (start > end) return -1;
    int mid = start + ((end - start) >> 1);
    if (arr[mid] == a) {
        return mid;
    } else if (arr[mid] > a) {
        end = mid - 1;
        return binarySearch2(arr, start, end, a);
    } else {
        start = mid + 1;
        return binarySearch2(arr, start, end, a);
    }
}

二、二分法查詢的侷限性

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

  • 首先,二分查詢依賴的是順序表結構,就是陣列

    • 二分查詢不能依賴連結串列。原因二分查詢演算法需要按照下標隨機訪問元素。
    • 陣列按照下標隨機訪問資料的時間複雜度是 O(1),而連結串列隨機訪問的時間複雜度是 O(n)。
    • 所以,如果資料使用連結串列儲存,二分查詢的時間複雜就會變得很高。
    • 二分查詢只能用在資料是通過順序表來儲存的資料結構上。如果你的資料是通過其他資料結構儲存的,則無法應用二分查詢。
  • 二分查詢針對的是有序資料

    • 二分查詢對這一點的要求比較苛刻,資料必須是有序的。如果資料沒有序,我們需要先排序。
    • 序的時間複雜度最低是 O(nlogn)。所以,如果針對的是一組靜態的資料,沒有頻繁地插入、刪除,我們可以進行一次排序,多次二分查詢。
    • 這樣排序的成本可被均攤,二分查詢的邊際成本就會比較低。
    • 如果資料集合有頻繁的插入和刪除操作,要想用二分查詢,要麼每次插入、刪除操作之後保證資料仍然有序,要麼在每次二分查詢之前都先進行排序。
    • 針對這種動態資料集合,無論哪種方法,維護有序的成本都是很高的。
    • 所以,二分查詢只能用在插入、刪除操作不頻繁,一次排序多次查詢的場景中。針對動態變化的資料集合,二分查詢將不再適用。
  • 資料量太大也不適合二分查詢。

    • 二分查詢的底層需要依賴陣列這種資料結構,而陣列為了支援隨機訪問的特性,要求記憶體空間連續,對記憶體的要求比較苛刻。
    • 比如,有 1GB 大小的資料,如果希望用陣列來儲存,那就需要 1GB 的連續記憶體空間。
    • 注意這裡的“連續”二字,也就是說,即便有 2GB 的記憶體空間剩餘,但是如果這剩餘的 2GB 記憶體空間都是零散的,沒有連續的 1GB 大小的記憶體空間,那照樣無法申請一個1GB大小的陣列。
    • 而二分查詢是作用在陣列這種資料結構之上的,所以太大的資料用陣列儲存就比較吃力了,也就不能用二分查詢了。

相關文章