1. 程式人生 > >二分查詢以及其有趣的使用

二分查詢以及其有趣的使用

說到二分查詢可能大家會想到二分查詢的基本模板類似這樣的:

 /**
     * 二分查詢的基本實現
     * 作用:對於一個已經從小到大排好序的陣列找出目標元素的索引值
     * 時間複雜度為O(logn)
     * @param nums
     * @param target
     * @return
     */
    public static int BinarySearch(int[] nums, int target){
        if(nums.length == -1)
            return -1;
        int left = 0;
        int mid = 0;
        int right = nums.length-1;
        while (left<=right){
            mid = left+(right-left)/2;
            if(nums[mid]==target)
                return mid;
            else if(nums[mid]<target)
                left = mid+1;
            else
                right = mid-1;
        }
        return -1;
    }

然而這篇博文我想和大家一起看看二分查詢在一些題目裡不一樣的用法

 

題目一:

來源LeetCode-278 第一個錯誤版本

你是產品經理,目前正在帶領一個團隊開發新的產品。不幸的是,你的產品的最新版本沒有通過質量檢測。由於每個版本都是基於之前的版本開發的,所以錯誤的版本之後的所有版本都是錯的。

假設你有 n 個版本 [1, 2, ..., n],你想找出導致之後所有版本出錯的第一個錯誤的版本。

你可以通過呼叫 bool isBadVersion(version) 介面來判斷版本號 version

是否在單元測試中出錯。實現一個函式來查詢第一個錯誤的版本。你應該儘量減少對呼叫 API 的次數。

示例:

給定 n = 5,並且 version = 4 是第一個錯誤的版本。

呼叫 isBadVersion(3) -> false
呼叫 isBadVersion(5) -> true
呼叫 isBadVersion(4) -> true

所以,4 是第一個錯誤的版本。 

這題一看到就能讓人聯想到要用二分,可是這是差第一個錯誤版本,不像以前查出某一個數的索引這該怎麼辦呢,下面給出一位博主對於該題使用二分查詢的思路

博文連結

思路部分如下:

因為一個版本是錯誤,其後面的所有版本都是錯誤的,所以我們可以用二分搜尋,當取中點時,如果中點是錯誤版本,說明後面都是錯誤的,那第一個錯誤版本肯定在前面。如果中點不是錯誤版本,說明第一個錯誤版本肯定在後面。

這裡直接使用min <= max的二分模板,因為我們其實要找的是好和壞的分界點,即這個點既不是好也不是壞,所以是找不到的,按照模板的特點,最後退出迴圈時,max指向小於目標的點,min指向大於目標的點,這裡第一個壞的version較大,所以返回min

 

怎麼樣是不是感覺我去二分查詢還能這麼用有點意思吧

附上博文連結裡大佬的java題解:

public class Solution extends VersionControl {
    public int firstBadVersion(int n) {
        int min = 1, max = n, mid = 0;
        while(min <= max){
            mid = min + (max - min) / 2;
            if(isBadVersion(mid)){
                max = mid - 1;
            } else {
                min = mid + 1;
            }
        }
        return min;
    }
}

 

題目二

LeetCode-287 尋找重複數

給定一個包含 n + 1 個整數的陣列 nums,其數字都在 1 到 之間(包括 1 和 n),可知至少存在一個重複的整數。假設只有一個重複的整數,找出這個重複的數。

示例 1:

輸入: [1,3,4,2,2]
輸出: 2

示例 2:

輸入: [3,1,3,4,2]
輸出: 3

說明:

  1. 不能更改原陣列(假設陣列是隻讀的)。
  2. 只能使用額外的 O(1) 的空間。
  3. 時間複雜度小於 O(n2) 。
  4. 陣列中只有一個重複的數字,但它可能不止重複出現一次。

 

這題我一拿到是矇蔽的,空間複雜度和時間複雜度都受到了限制,對於我這種業餘演算法選手是真的有點難受了,搜了下題解,又是二分查詢,其實我該想到的,O(1)的空間,小於O(n2)的時間,這些都算是LeetCode對於使用二分查詢的提示了

題解連結

題解作者的思路:

這道題給了我們n+1個數,所有的數都在[1, n]區域內,首先讓我們證明必定會有一個重複數,這不禁讓我想起了小學華羅庚奧數中的抽屜原理(又叫鴿巢原理), 即如果有十個蘋果放到九個抽屜裡,如果蘋果全在抽屜裡,則至少有一個抽屜裡有兩個蘋果,這裡就不證明了,直接來做題吧。題目要求我們不能改變原陣列,即不能給原陣列排序,又不能用多餘空間,那麼雜湊表神馬的也就不用考慮了,又說時間小於O(n2),也就不能用brute force的方法,那我們也就只能考慮用二分搜尋法了,我們在區別[1, n]中搜索,首先求出中點mid,然後遍歷整個陣列,統計所有小於等於mid的數的個數,如果個數大於mid,則說明重複值在[mid+1, n]之間,反之,重複值應在[1, mid-1]之間,然後依次類推,直到搜尋完成,此時的low就是我們要求的重複值.

 

好吧小菜雞拜倒在大佬的想法下了,行雲流水,理所當然,題解的博主是用C++寫的,下面附上我改的過了的java版程式碼:

class Solution {
    public int findDuplicate(int[] nums) {
        int low = 1, high = nums.length - 1;
        while (low < high) {
            int mid = low + (high - low) /2;
            int cnt = 0;
            for (int a : nums) {
                if (a <= mid) ++cnt;
            }
            if (cnt <= mid) low = mid + 1;
            else high = mid;
        }
        return low;
    }
}

 

上面這兩道題加深了我對二分查詢的認識,它確實不只是查詢那麼簡單,用好了的話可以很神奇的解決一些問題.

因為發現自己在和朋友說題時還沒理解透這兩個題目和其對於二分查詢的運用所以寫下這篇部落格備查.