【二分查找法】你真的寫對了嗎?

導語

在衆多有趣又有難度的題目中,有一道老題倒是你們都紛紛選擇避開的,那就是去實現二分查找。html

由於它很好寫,卻很難寫對。能夠想象問了這道題後,在5分鐘以內面試的同窗會至關自信的將那一小段代碼交給咱們,剩下的就是考驗面試官可否在更短的時間內看出這段代碼的bug了。 ---- ccmousejava

看起來是個很小的問題,其實也不容易。聽說第一篇二分搜索論文是1946年發表,可是徹底沒有錯誤的二分搜索程序倒是在1962年纔出現,用了16年的時間。可想而知,要想寫出一個基本沒有錯誤的二分搜索程序並不像看起來的那麼簡單。程序員

錯誤寫法示例

1.第一種寫法

public static int bs1(int a[], int x, int n) {
    int left = 0;
    int right = n - 1;
    while (left <= right) {
        int middle = (left + right) / 2;
        if (x == a[middle])
            return middle;
        if (x > a[middle])
            left = middle;
        else
            right = middle;
    }
    return -1;
}

2.第一種寫法評析

[x] 是錯誤的
當你輸入bs1(new int[]{1,2,3,4,5},9,5)時,你會神奇的發現你的代碼卡着不動了(其實是陷入了死循環)。面試

仔細分析一下代碼,當x>middle時,left=middle,這會形成一個很大的問題,舉個栗子,當你left=3,right=4,middle=3時,不管循環多少次,left始終爲3,沒法繼續查找,也就是以前陷入的死循環。
同理right=middle也會形成一樣的錯誤。編程

並且這裏還有一個問題,當個人數組特別特別大,例若有2147483646個元素,由於整數表示的範圍有限,爲-2147483648到2147483648,那麼上述代碼中的int middle = (left + right) / 2;語句就有可能會發生溢出,實際測試會拋出以下異常:數組

Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
    at bin_search.Demo1.main(Demo1.java:21)

3.第二種寫法

public static int bs2(int a[], int x, int n) {
    int left = 0;
    int right = n - 1;        
    while (left < right -1) {
        int middle = (left + right) / 2;
        if (x < a[middle]) 
            right = middle;
        else 
            left = middle;
    }
    if (x == a[left]) return left;
    return -1;
}

4.第二種寫法評析

[x] 是錯誤的
舉個栗子驗證一下,輸入bs2(new int[]{1,2,3,4,5},5,5),居然返回-1!說明該方法不能查找到數組尾元素。dom

仔細分析一下代碼,咱們發現除了和以前同樣可能存在整數溢出的錯誤以外,還存在一個沒法查找尾元素的bug,查找的最後幾步過程以下:函數

運行流程

因此最後一個元素就被「略」過了,因此這個寫法就不能獲得正確的結果。測試

正確寫法

看起來,二分搜索雖然思路簡單可是很難寫對。《編程珠璣》的做者Jon Bentley曾經收集過學生的代碼,發現其中有90%都是錯的,甚至連之前java的庫中,二分搜索也存在着一個隱藏了10年的嚴重bug。
若是感興趣的話能夠看下面這篇文章的詳細介紹:spa

二分查找--那個隱藏了10年的Java Bug

咱們來看看官方的二分搜索法是怎麼實現的。

public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list, T key) {
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
            return Collections.iteratorBinarySearch(list, key);
}

private static <T> 
    int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key) {
        int low = 0;
        int high = list.size()-1;

        while (low <= high) {
            int mid = (low + high) >>> 1;
            Comparable<? super T> midVal = list.get(mid);
            int cmp = midVal.compareTo(key);

            if (cmp < 0)
                low = mid + 1;
            else if (cmp > 0)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found
}

private static <T>
    int iteratorBinarySearch(List<? extends Comparable<? super T>> list, T key) {
        int low = 0;
        int high = list.size()-1;
        ListIterator<? extends Comparable<? super T>> i = list.listIterator();

        while (low <= high) {
            int mid = (low + high) >>> 1;
            Comparable<? super T> midVal = get(i, mid);
            int cmp = midVal.compareTo(key);

            if (cmp < 0)
                low = mid + 1;
            else if (cmp > 0)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found
}

上面的low就對應left,high對應right。

可是對於一些很是大的數組進行二分查找問題,就連Java的庫函數也沒法勝任。
由此看來,二分搜索須要從新更好地設計才能適應超大型的數組。(固然,若是真有那麼大的數組,咱們是不會用二分搜索的,由於它太慢了)

總結

總結一下,二分搜索須要注意的點有如下幾條:

  1. 數組必定記得要先排序!!!(不排序會出現各類莫名其妙的返回值)
  2. 取中位值的時候,須要注意整數加法是否會溢出的問題。
  3. 當查找不在數組內的元素時,須要返回-1表明沒有找到。
  4. 若是出現待查找元素有重複的元素,須要肯定返回的是哪個元素的下標。

參考資料

相關文章
相關標籤/搜索