二分搜索相關算法題彙總

二分搜索簡介

在計算機科學中,二分搜索(binary search)也稱折半搜索(half-interval search)、對數搜索(logarithmic search),是在有序數組中查找某一特定元素的搜索算法。

二分搜索是一種在每次比較之後將查找空間一分爲二的算法。每次需要查找集合中的索引或元素時,都應該考慮二分搜索。如果集合是無序的,我們可以總是在應用二分搜索之前先對其進行排序。

二分搜索的時間複雜度是 O(log n),空間複雜度爲 O(1)。

如何判斷是否需要使用二分搜索,當你看到時間複雜度是 O(log n)就要考慮能夠用二分搜索或是使用分治

二分搜索模板

  • 終止條件,左右指針合併或是找到目標值
  • 目標值小於mid,在左,right變爲mid-1
  • 目標值大於mid,在右,left變爲mid+1
  • 可以理解爲每次判斷丟掉一半,排除在外
int binarySearch(int[] nums, int target){
  if(nums == null || nums.length == 0)
    return -1;

  int left = 0, right = nums.length - 1;
  while(left <= right){
    int 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;
}

704. 二分查找

這個題目直接套用模板就行:

    public int search(int[] nums, int target) {
        if(nums == null || nums.length == 0)
            return -1;

        int left = 0, right = nums.length - 1;
        while(left <= right){
            int 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;
    }

變形

二分搜索的算法題目都是通過最基礎的方法演變而來:

返回值再處理

最普遍的變形就是後處理,像下面這題就是,先二分查找到符合的target的index,通過index中心擴散找到邊界,返回

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

先編寫一個函數用來求一點周圍相同數值的邊界:

    private int[] findSame(int[] nums, int mid){
        int left = mid-1, right=mid+1;
        while (left>=0 && nums[left]==nums[mid]) left--;
        while (right<nums.length && nums[right]==nums[mid]) right++;
        return new int[]{left+1, right-1};
    }

然後匹配到mid之後直接將mid值帶入函數即可求解:

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

        return res;
    }
    
    private int[] findSame(int[] nums, int mid){
        int left = mid-1, right=mid+1;
        while (left>=0 && nums[left]==nums[mid]) left--;
        while (right<nums.length && nums[right]==nums[mid]) right++;
        return new int[]{left+1, right-1};
    }
}

目標值需要邏輯處理

有些二分查找並不是查找的輸入值,而是輸入只通過處理後的到的值

69. x 的平方根

  • 因爲需要只保留整數部分就需要判斷目標值是否介於mid2和(mid+1)2之間
class Solution {
    public int mySqrt(int x) {
        long left = 0;
        long right = x / 2;

        while (left <= right) {
            long mid = left + (right - left) / 2;
            long sqr = mid * mid; 
            long nextSqr = (mid + 1) * (mid + 1); 
 
            if (sqr == x || (sqr < x && nextSqr > x)) {
                return (int) mid;
            } else if (sqr < x) {
                left = mid + 1;
            } else if (sqr > x) {
                right = mid - 1;
            }
        }
        return x;
    }
}

臨界點討論

正常的二分搜索是有序,有些雖然有序但是出現了斷點,對於斷點就需要分段考慮:

153. 尋找旋轉排序數組中的最小值

  • 找到第一個左大於右的那個點即爲臨界點
    public int findMin(int[] nums) {
        int left = 0;
        int right = nums.length - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > nums[right]) {          
                left = mid + 1;
            } else {                                
                right = mid;
            }
        }
        return nums[left];
    }

類似的問題還有:

852. 山脈數組的峯頂索引

  • 相對於上一題,這個要找的是A[i] < A[i+1] 的最大 i
    public int peakIndexInMountainArray(int[] A) {
        int left = 0;
        int right = A.length - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (A[mid] > A[right]) {          
                left = mid + 1;
            } else {                                
                right = mid;
            }
        }
        return left;
    }

下面是一個循環數組的進階變形:

33. 搜索旋轉排序數組

  • 先根據 nums[mid] 與 nums[lo] 的關係判斷 mid 是在左段還是右段
  • 再判斷 target 是在 mid 的左邊還是右邊,從而調整左右邊界 lo 和 hi
public int search(int[] nums, int target) {
    int lo = 0, hi = nums.length - 1, mid = 0;
    while (lo <= hi) {
        mid = lo + (hi - lo) / 2;
        if (nums[mid] == target) {
            return mid;
        }
        // 先根據 nums[mid] 與 nums[lo] 的關係判斷 mid 是在左段還是右段 
        if (nums[mid] >= nums[lo]) {
            // 再判斷 target 是在 mid 的左邊還是右邊,從而調整左右邊界 lo 和 hi
            if (target >= nums[lo] && target < nums[mid]) {
                hi = mid - 1;
            } else {
                lo = mid + 1;
            }
        } else {
            if (target > nums[mid] && target <= nums[hi]) {
                lo = mid + 1;
            } else {
                hi = mid - 1;
            }
        }
    }
    return -1;
}

類似的題目還有:

1095. 山脈數組中查找目標值

這個題需要用到兩次二分,先是二分找頂點分成兩端,再二分找最小值

  • 首先因爲山的特性,使數組分爲兩部分,一部分是升序,一部分是降序
  • 通過二分查找,確定山頂位置,將數組分爲兩段
  • 由於是求最小的index,故先二分查找升序山脈,如果沒有,查找降序山脈,最終返回結果
class Solution {
    public static int findInMountainArray(int target, MountainArray mountainArr){
        int left=0, right=mountainArr.length()-1;
        while (left+1!=right){
            int mid = (left+right)>>1;
            if(mountainArr.get(mid)>mountainArr.get(mid-1)) left=mid;
            else right=mid;
        }
        int top = mountainArr.get(left)>mountainArr.get(right)?left:right;
        int leftRes = binSearch(0, top, target, mountainArr,1);
        if (leftRes!=-1) return leftRes;
        else return binSearch(top+1, mountainArr.length()-1, target, mountainArr, -1);
    }
    
    private static int binSearch(int left, int right, int target, MountainArray mountainArr, int asc){
        while (left <= right) {
            int mid = (left+right)>>1;
            int value = mountainArr.get(mid);
            if (value==target) return mid;
            if ((target-value)*asc>0) left=mid+1;
            else right=mid-1;
        }
        return -1;
    }
}

兩個數組求中位數

4. 尋找兩個正序數組的中位數

  • 題目本身不難但是要求時間複雜度就得用二分做
  • 本題的想法是通過一次判斷丟掉一半內容
  • 最終搜索的因爲奇偶分兩種情況需要注意
class Solution {
    public static double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int len1 = nums1.length, len2 = nums2.length, len = len1+len2;
        if (len<=1) return len1==0? nums2[0]:nums1[0];
        boolean odd = (len&1)==0;
        int i1=0, i2=0, k=(len+1)/2;
        while (true) {
            if (i1 == len1) return odd ? (nums2[i2+k-1] + nums2[i2+k])/2D : nums2[i2+k-1];
            if (i2 == len2) return odd ? (nums1[i1+k-1] + nums1[i1+k])/2D : nums1[i1+k-1];
            if (k ==1){
                if (odd) {
                    if (nums1[i1]<nums2[i2]) {
                        return i1 == len1-1 ? (nums1[i1]+nums2[i2])/2D:(nums1[i1] + Math.min(nums2[i2], nums1[i1+1]))/2D;
                    }else{
                        return i2 == len2-1 ? (nums1[i1]+nums2[i2])/2D:(nums2[i2] + Math.min(nums2[i2+1], nums1[i1]))/2D;
                    }
                }else{
                    return Math.min(nums1[i1], nums2[i2]);
                }
            }
            int mid = k/2;
            int n1 = Math.min(i1 + mid, len1) -1;
            int n2 = Math.min(i2 + mid, len2) -1;
            if (nums1[n1] <= nums2[n2]) {
                k -= n1 - i1 + 1;
                i1 = n1 + 1;
                while (nums2[i2]<nums1[n1]){
                    i2++;
                    k--;
                }
            } else {
                k -= n2 - i2 + 1;
                i2 = n2 + 1;
                while (nums1[i1]<nums2[n2]){
                    i1++;
                    k--;
                }
            }
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章