二分搜索簡介
在計算機科學中,二分搜索(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--;
}
}
}
}
}