算法之二分法及其應用


算法之二分法及其應用

算法思路

① 將數組中間元素與目標元素進行比較,如果正好是目標元素,則結束搜索

② 如果目標元素大於中間元素,則進入中間元素的右邊區域進行查找,重復步驟 ① 的操作

③ 如果目標元素小於中間元素,則進入中間元素的左邊區域進行查找,重復步驟 ① 的操作

依次類推,若某一步查找區域為空,則表明沒有找到目標元素

代碼實現

    public int binarySearch(int[] arr, int key) {
        int low = 0;
        int high = arr.length - 1;
        while (low <= high) {
            int mid = low + (high - low) / 2; 
            if (key == arr[mid]) {
                return mid;
            } else if (key > arr[mid]) {
                low = mid + 1;
            } else {
                high = mid - 1;
            }
        }
        return -1;  // 表明沒有找到對應的值
    }

提醒:這里求 mid 的時候不推薦使用 mid = ( low + high ) / 2 的方式,因為當數組較大的時候,low + high 可能會發生溢出

推薦寫成 mid = low + (high - low) / 2

( 除 2 等同於右移一位,所以也可以寫成 mid = low + ((high - low) >> 1),右移一位比除 2 的速度要快)

我曾思考過既然要防止溢出,意味着也可以寫成 mid = low / 2 + high / 2 ,不過這種方式算出來沒有上述代碼中所寫方式更精確,譬如 low 為 1,high 為 3, 那么通過這種方式得到的就是 1/2 + 3/2 = 0 + 1 = 1, 而 1 和 3 的中間數應該是 2

復雜度分析

通過比較中間值,每次都可以過濾掉一半的元素,所以時間復雜度為 O ( log N )

應用

1. 找到等於 num 的值 [前提:有序]

這是最常見的應用,代碼如上所示

2. 找到大於等於 num 最左側的位置 [前提:有序]

題目

舉個栗子,比如 int[] arr = {1, 2, 3, 3, 4, 4, 7, 7, 9} , 那么大於等於 3 的最左側位置下標為 2,大於等於 5 的最左側位置下標為 6

思路

① 將數組中間元素與 num 進行比較

A. 如果中間元素滿足 >= num , 則將此下標保存到變量 t 中,並繼續中間元素左側的二分

B. 如果中間元素不滿足 >= num , 則繼續中間元素右側的二分

按照 ① 步驟不斷二分直到二分結束

代碼

   public int findLeftest(int[] arr, int num) {
        int low = 0;
        int high = arr.length - 1;
        int t = -1;
        while (low <= high) {
            int mid = low + (high - low) / 2;
            if (arr[mid] >= num) {
                t = mid;
                high = mid - 1;
            } else {
                low = mid + 1;
            }
        }
        return t;
    }

總結

找到等於 num 的位置和找到大於等於 num 最左側的位置最大的不同在於,前者只要找到目標元素的位置,就可以返回;而后者必須要二分結束,才能返回

相似應用

找到小於等於 num 最右側的位置,實現思路和上面是差不多的,有興趣的同學可以自己試着寫一下

3. 局部最小值問題

題目

在數組中找到一個局部最小的位置
定義局部最小的概念。arr 長度為1時,arr[0] 是局部最小。arr 的長度為 N(N>1) 時,如果 arr[0] < arr[1],那么 arr[O] 是局部最小;如果 arr[N-1] < arr[N-2] ,那么 arr[N-1] 是局部最小;如果 O<i<N-1 ,既有 arr[i] < arr[i-1] ,又有 arr[i] < arr[i+1] ,那么 arr[i] 是局部最小。給定無序數組 arr,已知 arr 中任意兩個相鄰的數都不相等。寫一個函數,只需返回 arr 中任意一個局部最小出現的位置即可。

思路

A. 先判斷 arr[0] 是否為局部最小,是則返回

B. 看最后一位是否為局部最小,是則返回

C. 若都不是,那么在 0 ~ N-1 之間必存在局部最小

說明如下:由於 arr[0] 和 arr[n-1] 都不是局部最小,那么 arr[0] > arr[1] ,arr[N-1] > arr[N-2], 數組呈現的趨勢如下:

圖片說明1

那么在這數組中,一定存在局部最小值(無論中間的趨勢如何,一定會出現一個波谷,因為最左邊是呈下降趨勢,最右邊呈上升趨勢,其中就至少存在一個位置,使得趨勢發生變化,而發生變化的這個點就是波谷,即我們的局部最小)

D. 取中間值 M, 判斷 M 是否為局部最小,若是則返回;否則,arr[M] > arr[M-1] 或 arr[M] > arr[M+1],假設arr[M] > arr[M-1],由假設情況可得到如下圖

圖片說明 2

E. 那么 0 ~ M 之間必定存在局部最小,依次類推... 必能找到局部最小值

代碼

  public int findMiniNum(int[] arr) {
        int length;
        if (arr == null || (length = arr.length) < 1) {
            return -1;  // 表示不存在
        }
        // 判斷 arr[0] 是否為局部最小
        if (length == 1 || arr[0] < arr[1]) {
            return 0;
        }
        // 判斷最后一個元素是否為局部最小
        if (arr[length - 1] < arr[length - 2]) {
            return length - 1;
        }
        int low = 1;
        int high = length - 2;
        while (low < high) {
            int mid = low + (high - low) / 2;
            if (arr[mid] > arr[mid - 1]) {
                high = mid - 1;
            } else if (arr[mid] > arr[mid + 1]) {
                low = mid + 1;
            } else {
                return mid;
            }
        }
        return low;	// 此時 low == high,而必定存在局部最小,所以直接返回 low 或 high 即可
    }

總結

我們慣性思維是二分法只能用於有序數組,這道題打破了我們的常規印象

歡迎大家來我博客逛逛 mmimo技術小棧


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM