[刷題]從新審視二分查找算法的代碼實現

從新審視二分查找

去年九月份投了人生第一份簡歷,百度地圖公共出行部門的實習生,就抱着感覺面試+碰運氣的態度去面試了(結果可想而知,哈哈哈...Emm..面試官都挺好的,只不過三個部門的老大輪流面,每位大佬都面了一個小時可把我面死了,<ps:我猜想由於每一個部門的老大都不想要我,因此都給別的部門來看一看> 並且我仍是人生第一次面試, T.T) 其中第二位面試官問個人算法題讓我手撕 二分查找,當時還想,誒?這個面試官這麼好,出的題好簡單。如今刷了100道leetcode之後,再來審視這個問題發現,並不簡單...

其實你們也可能有和我同樣的想法,不就是二分查找嗎,有什麼可難的,
好,如今在沒有複習的狀況下我問你們幾個問題,你們自查一下: 循環的條件是left<right仍是left<=right?二分時是left=mid+1仍是left=mid 一樣的是right = mid-1仍是right=mid?對於上面全部的問題:爲何這樣作?面試

若是你可以對每一步的作法都道的清楚,那麼恭喜你,這篇給像我這樣的小菜菜看和複習的文章您能夠跳過啦!算法

可是若是你和我同樣,爆了粗口F&%^!k,好的,先握個手,咱們先一塊兒去學習一下liwei大佬如何理解這個問題(篇幅較長,耐心消化):
https://www.liwei.party/2019/... 數組

正文

好了啦,其實我這篇文章也只是對他的一個自我消化,方便我往後複習,以及複習我失敗的面試經歷(臥薪嚐膽啊懂不懂!)函數

liwei大佬的方法我以爲有這麼三點比較重要:學習

  1. 不斷夾逼目標值,經過left<right,避免不知道循環結束後選哪一個邊界的尷尬。
  2. 先寫好判斷的邏輯(即知足哪一個條件必定會怎麼怎麼樣,這一步每每是最重要也最難的部分)
  3. 根據邏輯選擇mid(偶數時選左邊仍是右邊)

多說無益,直接上題:測試

Leetcode704: 二分查找code

給定一個 n 個元素有序的(升序)整型數組 nums 和一個目標值 target,
寫一個函數搜索 nums 中的 target,若是目標值存在返回下標,不然返回 -1。

示例 1:
輸入: nums = [-1,0,3,5,9,12], target = 9
輸出: 4
解釋: 9 出如今 nums 中而且下標爲 4

學會了以後先練個手,這很簡單吧,咱們這裏把思考過程寫清楚一點
這裏目標值target也給出了(PS:有的題目須要本身思考目標值),
1.首先肯定左邊界和右邊界 left = 0; right = nums.length-1;
2.寫出while(left<right){}
3.肯定分支邏輯.對於這一題咱們能夠想到若是nums[mid]<target,那麼mid及mid左側的元素裏必定不存在目標值,咱們縮減區間爲left = mid+1;;那麼相應的另外一個分支爲num[mid]>=target,這個分支的條件下 mid及mid左側的元素均可能是target(或者說mid右側的元素都不多是target),咱們縮減區間right = mid;(對於這一題,其實先從nums[mid]>target的角度思考是同樣的啦)
4.咱們的分支邏輯中包括了left=mid+1,爲了不死循環,咱們須要定義mid = left + (right-left)/2 (也就是當剩下偶數個元素時,選擇左邊的那個mid,同理當你的二分邏輯爲right=mid-1時,須要定義mid = left + (right-left+1)/2)
5.完成循環後,left==right,此時選left和right都是同樣的.但因爲target可能不存在數組中(能夠簡單思考一下這時候left指向了哪裏),所以須要特判一下 return nums[left]==target?left:-1;
完整代碼以下:排序

class Solution {
    public int search(int[] nums, int target) {
        int len = nums.length;
        int left =0;
        int right=len-1;
        if(len==0) return -1;
        
        while(left<right){
            int mid = left + (right-left)/2;
            if(nums[mid]<target){
                left = mid+1;
            }
            else{
                right = mid;
            }
        }
        return nums[left]==target?left:-1;

    }
}

來再作個應用題
Leetcode69: x的平方根索引

實現 int sqrt(int x) 函數。
計算並返回 x 的平方根,其中 x 是非負整數。
因爲返回類型是整數,結果只保留整數的部分,小數部分將被捨去。

示例 1:
輸入: 4
輸出: 2

示例 2:
輸入: 8
輸出: 2
說明: 8 的平方根是 2.82842..., 
     因爲返回類型是整數,小數部分將被捨去。

這一題求x的平方根取整,你固然能夠用庫函數,或者遍歷O(N)的複雜度,哈哈哈,可是咱不是在練習嘛,來來來,咱們用O(logN)時間複雜度,二分的方法作一下。
都是套路,繼續上模板
1.首先肯定左邊界和右邊界 left = 1; right = x;這裏right爲啥能夠等於能夠取到x了呢,由於一個數的平方根多是它本身啊(不用看了,1,說的就是你)
2.寫出while(left<right){}
3.肯定分支邏輯.注意,這裏由於咱們要找平方根,因此咱們的目標是求一個mid使得( mid * mid ) == x .而且根據題意,小數部分被捨棄,即向下取整,那麼使得i * i <=x 的 i 值都有多是所求,但能夠肯定的是,使得i * i>x 的 i 值必定不是所求。據此,咱們能夠寫出分支邏輯:當
mid * mid > x時,mid及mid右側的元素必定不包括目標值,縮減區間爲right = mid - 1;;那麼相似於上一個題目,稍(套)加(用)思(模)考(板)另外一else分支爲left = mid
4.因爲本題目的目標值必定存在,所以最後不須要特判返回便可.
完整代碼以下:leetcode

class Solution {
    public int mySqrt(int x) {
        // 0 特判一下
        if(x==0) return 0;
        // 測試用例中存在大整數,所以用long
        long left = 1;
        long right = x;
        long mid;
        while (left<right){
            mid = left + (right-left+1)/2;
            if(mid*mid>x){
                right = mid-1;
            }
            else {
                left = mid;
            }
        }
        return (int)left;
    }
}

"老闆!不過癮,再來一碗酒(題)!"
"呦,客官,您看咱們的招牌上寫了‘三碗不過岡’啊.."
行吧,既然你這麼要求了

Leetcode35: 搜索插入位置

給定一個排序數組和一個目標值,在數組中找到目標值,
並返回其索引。若是目標值不存在於數組中,返回它將會被按順序插入的位置。

你能夠假設數組中無重複元素。

示例 1:
輸入: [1,3,5,6], 5
輸出: 2

示例 2:
輸入: [1,3,5,6], 2
輸出: 1

其實這道題目和第一題大差不差,只不過這裏數組裏不存在目標值時還要求返回應該插入的位置.
由於邏輯判斷的思路基本一致,因此再也不贅述思考過程了,這裏只討論兩個不同的點
1.初始搜索區間爲left=0; right=nums.lenght,由於須要找到插入的位置,所以須要把右邊界擴展

你可能會想:誒?當right=nums.length難道這樣不會越界嗎?
答:可能會。區間收縮的過程當中,可能收縮到這:[nums.length-1,nums.length], 而此時若是你選擇了nums.length爲mid 那麼就會出現越界.但若是你選擇了左邊的爲mid,緊接着left=mid+1, 此時left==right==nums.length,跳出循環,因此你選擇左邊的mid就不會出現越界

2.若是數組中不存在目標值,跳出循環時,邊界指向哪裏?在第一題中我也讓你們思考了這個問題。如今咱們來討論一下,能夠確定的是,此時兩個邊界值相同,且必定指向第一個小於目標值的元素或者第一個大於目標值的元素,那具體指向哪個呢?答案是包含了mid的那個分支條件,更具體來講即left=midright=mid中的其一,對應的即第一個小於目標值的元素或第一個大於目標值的元素.由於mid一直包含在這個分支中。

完整代碼以下:

public class Solution{
    public static int searchInsert(int[] nums, int target) {
        if(nums.length==0) return 0;
        int left = 0;
        int right = nums.length;
        while (left<right){
            int mid = left + (right-left)/2;
            // 若是mid 小於target ,mid及mid左側必定不是
            if(nums[mid]<target){
                left = mid+1;
            }
            // 若是 mid 大於等於target, mid或mid左側多是,但右側必定不是
            else {
                right = mid;
            }
        }
        // 由於在變化的是左邊界,而右邊界包含了mid,
        // 最後邊界會收縮到右邊界的分支,即target的索引(找到了)或第一個>target的索引(沒找到)
        // 而沒找到時,這個索引恰好是應該插入的位置
        return left;
    }
}

這段代碼等價於:

public int searchInsert(int[] nums, int target) {
        if(nums.length==0) return 0;
        if(target<nums[0]) return 0;
        if(target>nums[nums.length-1]) return nums.length;
        int left = 0;
        int right = nums.length-1;
        while (left<right){
            int mid = left + (right-left+1)/2;
            // 若是mid 大於target ,mid及mid右側必定不是
            if(nums[mid]>target){
                right = mid-1;
            }
            // 若是 mid 小於等於target, mid或mid右側多是,但左側必定不是
            else {
                left = mid;
            }
        }
        // 由於在變化的是右邊界 因此最後區間會收縮到左邊界的條件
        // 所以咱們須要判斷一下
        return nums[left]==target?left:left+1;
    }

第二段代碼換了一個起始的思考過程,卻同時帶來了各類麻煩,咱們須要各類特判來解決麻煩。形成麻煩的緣由就在於跳出循環時,若該元素不存在,邊界的指向位置在應插入位置的左側。
你們能夠經過nums=[1,2] target=0; nums=[1,2] target=3這個兩個例子本身手動模擬一遍兩種不一樣的方式,深入理解邊界的停留位置。

未完待續....

相關文章
相關標籤/搜索