【算法】二分查找


二分查找

1.概念

如果想要在數組中查找一個數,最基本的方法就是暴力解法:一次遍歷,這時候時間復雜度是O(N),二分查找就是其中的一種優化,時間復雜度是O(logN);具體做法是一步一步逼近直到找到。前提是數組需要是一個排序數組

定義:二分查找也稱折半查找(Binary Search),是一種在有序數組中查找某一特定元素的搜索算法。我們可以從定義可知,運用二分搜索的前提是數組必須是有序的,這里需要注意的是,我們的輸入不一定是數組,也可以是數組中某一區間的起始位置和終止位置

2.過程

  • 1.二分查找先初始化一個搜索空間,然后再這個搜索空間上去尋找某個值,注意這個搜索空間是有某種規律,比如說是有序的;
  • 2.因為這種規律,直接去看中間值,如果判斷中間值和我們想要的結果的關系,比如搜索數字,看中間值和目標值的大小關系;
  • 3.如果mid>t,那證明應該去前半部分搜索;如果mid<t,那說明該去前半部分搜索;
  • 4.然后接着在新的搜索空間執行2,3;

關鍵
Knuth 大佬(發明 KMP 算法的那位)怎么說的:
Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky...
思路很簡單,細節是魔鬼

3.模板

//循環寫法;
public int binarySearch(int[] nums, int target){
    int left = 0, right = num.length-1;
    while(left <= right){  //細節1:循環條件;
        int mid = left + ((right-left) >> 1);  
        //細節2:防止溢出,此外需要注意由於優先級的原因,需要添加括號;
        if(nums[mid] == target){
            return mid;
        }else if(nums[mid] < target){
            left = mid + 1; //細節3:注意加減1;
        }else{
            right = mid - 1;
        }
    }
    return -1
}

//遞歸寫法:
public int binarySearch(int[] nums, int target, int left, int right){
    if(left <= right){
        int mid = left + ((right-left)-1);
        if(nums[mid] == target){
            return mid;  //查找成功;
        }else if(nums[mid] > target){
            return binarySearch(nums, target, left, mid-1); //新的區間:左半區間;
        }else{
            return binarySearch(nums, target, min+1, right); //新的區間: 右半區間;
        }
    }
    return -1;
}

上述功能就是如果能在數組中找到目標值,就返回其索引,如果找不到,就返回其下標;如果目標值比中間值還大,那肯定在中間值右側(因為數組已經排序好了),如果目標值比mid值小,那肯定在mid左側。

  • 細節1:為什么while循環的條件時<=?
    因為我們初始化的時候右側區間是nums.length-1;所以是包括right的,即我們的區間是[left,right],這樣一個左閉右閉的區間,把這個區間理解成搜索區間,即我們是在這樣一個區間上搜索,那什么時候停止呢,兩個原因:
    • 1.找到了目標值,那就停止;
    • 2.沒找到目標值,但是搜索區間為空了,沒得找了,這時候停止;
      所以在最后一個=的時候,比如[2,2]這時候區間還不為空,萬一就是這個2呢。
  • 細節2:為什么寫成left+((right-left) >> 1);
    這主要是為了防止溢出,記住就可以了,注意除以2,用位運算的話會比較快一點,而且記得帶外面那個大括號;
  • 細節3: 為什么left = mid + 1,right = mid - 1?
    想一下剛才搜索區間的概念,如果發現了索引mid不是要找的target,那自然要從將mid剔除掉,從mid的左邊或者右邊找起來了。

缺陷:上述算法存在一個缺陷就是不能返回左右側邊界,比如數組是[1,2,2,2,3], target是2,這時候返回的索引是2,沒有辦法返回左右邊界。

4.樣例

34. 在排序數組中查找元素的第一個和最后一個位置

33. 搜索旋轉排序數組

劍指 Offer 53 - II. 0~n-1中缺失的數字

仔細體會上面3個樣例。

  • 1.解決了上面說到的不能返回左右邊界的問題;
  • 2.這個問題是不完全有序數組的二分查找;基本思路就是將其往排序數組上趕,比較mid和left來確定是前半段有序還是后半段有序;
  • 3.是二分查找的一個問題,並不僅僅是查找某個值某個元素,重點去感受最后的left=right=mid;體會最后跳出循環的返回值;

5.體會

  • 當寫成這樣時:返回的left是第一個>=target的值的索引。
    • 如果原數組有target返回的就是第一個target的索引;
    • 如果沒有那就是第一個比target大的值的索引(或者可以理解為要將target插入的位置索引);

之所以有上面的結論就在於最后一步一定是left=right=mid,而且mid左側都<t,mid右側都>=t,這時候執行判斷,如果mid大於等於target,那就返回它了,也就是left,如果<target,那就執行left+1,返回的就是第一個>=target的值;

while(left <= right){
    int mid = left + ((right-left) >> 1);
    if(target <= nums[mid]){ //將等於合並過來;
        right = mid-1;
    }else{
        left = mid+1;
    }
}
    return left; //第一個比大於等於target的索引;
  • 注意查看上面中提到的二分法的模板和細節,注意什么時候帶等號,什么時候不帶等號;
  • 二分查找本質上就是一個不斷縮小搜索空間的過程,比如我們找出某個值,就是在不斷的把空間縮小;找出哪個數字亂序,也在不斷的把空間縮小;求一個數的開根號,不斷的縮小空間然后拿值去逼近。這個過程要多想一下,就是我們不斷的縮小空間,然后每次都是拿這個空間上的中間值去做某種判斷,然后去逼近我們的結果
  • 二分查找應用的前提就是一定是一個有序的,或者半有序的,如果是半有序的話原則是就是不斷向有序上去趕,因為只有在有序的時候才能去二分;
  • 上面提供的二分查找的模板要始終明白在最后一步跳出循環前兩點:
    • 1.最后一步一定是right=left=mid。
    • 2.mid左側和右側一定是已經有了規律的了。比如查找值的時候,mid左側都比t值小,mid右側都比t值大;比如判斷從哪個數字開始亂了,mid左側一定是整齊的,mid右側一定是亂的。那這時候只要去判斷一下mid的值就可以了。如果mid>t或者說mid亂了,那正好返回left也就是mid,如果mid<t或者說mid沒亂,那left=mid+1,這不就正好是第一個大於t或者第一個亂的了嗎。


免責聲明!

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



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