数组可以快速使用二分查找法的条件:
- 数组为有序数组(必须具备,否则换思路)
- 数组无重复元素(最好具备,否则查找的index无法确定是哪一个target)
一、二分查找无重复元素数组
704. 二分查找(简单难度)
class Solution {
public:
// 定义target在区间[left,right]
int search1(vector<int>& nums, int target) {
int left = 0;
int right = nums.size()-1;
while (left <= right) {
int mid = left + ((right-left) / 2);//等价于(left + right)/2,防止溢出
if(nums[mid]==target) return mid;
else if(nums[mid]>target) right = mid - 1;
else left = mid + 1;
}
return -1;
}
// 定义target在区间[left,right)
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size();
while (left < right) {
int mid = left + ((right-left) / 2);//等价于(left + right)/2,防止溢出
if(nums[mid]==target) return mid;
else if(nums[mid]>target) right = mid;
else left = mid + 1;
}
return -1;
}
};
35. 搜索插入位置
// 定义target在区间[left,right)
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size();
while (left < right) {
int mid = left + ((right-left) / 2);//等价于(left + right)/2,防止溢出
if(nums[mid]==target) return mid;
else if(nums[mid]>target) right = mid;
else left = mid + 1;
}
return -1;
}
二、二分查找算数平方根
69. x 的平方根
class Solution {
public:
/* 本题是求解x的算数平方根,难点在于不准使用内置函数和运算符。
思路:二分查找+除法比较
x的算数平方根的范围只能是[1,x],只能遍历查找哪一个是平方根
这是一个有序数组,显然二分查找更加快捷
这道题注意,不能使用mid*mid和x比较,因为x的数值比较大,容易溢出,可以x/mid和mid比较*/
int mySqrt(int x) {
int left=1;
int right=x;
while(left<=right){
int mid=left+(right-left)/2;
if(x/mid==mid) return mid;
else if(x/mid<mid)right=mid-1;
else left=mid+1;
}
return right;
}
};
367. 有效的完全平方数
class Solution {
public:
/*解析:本题的题意是判断一个数是否是完全平方数
难点在于输入的num的范围非常大。
显然一个完全平方数必须可以被某个数整除,即在[1,num]间是否存在一个数可以被num整除
由于num非常大,所以查找必须更加快捷,只能用二分查找法。
当一个数满足num/i==i&&num%i==0时才是完全平方数,
注意num/i==i不能保证num就是完全平方数,因为num/i可能会舍弃小数
*/
bool isPerfectSquare(int num) {
int left=1;
int right=num;
while(left<=right){
int mid=left+(right-left)/2;
// 三种情况,注意相等的判断条件
if(num%mid==0&&num/mid==mid) return true;
else if(num/mid>mid) left=mid+1;
else right=mid-1;
}
return false;
}
};
三、二分查找含重复元素的数组
34. 在排序数组中查找元素的第一个和最后一个位置(中等难度)
class Solution {
public:
/*解析:本题是在一个排序数组中查找,首选二分查找法。
本题的难点在于数组中存在重复元素,传统的二分查找法可以找到其中某一个target,但不能判断是哪一个
可以巧妙地在传统二分查找得到的index上试着左移或右移来找到边界。
*/
vector<int> searchRange(vector<int>& nums, int target) {
int index=binarySearch(nums,target);//尝试二分查找
if(index==-1) return {-1, -1};// 数组中不存在target
// 此时数组中存在target,但是不确定有几个target,index指向其中的一个target
// 寻找左边界,尝试多次移动左边界
int leftBorder=index;
while(leftBorder-1>=0&&nums[leftBorder-1]==target)leftBorder--;
// 寻找右边界,尝试多次移动右边界
int rightBorder=index;
while(rightBorder+1<nums.size()&&nums[rightBorder+1]==target)rightBorder++;
return {leftBorder, rightBorder};
}
private:
// 二分查找函数[left,right]
int binarySearch(vector<int>& nums, int target) {
int left = 0;
int right = nums.size()-1;
int res = nums.size(); // 记录一下leftBorder没有被赋值的情况
while (left <= right) {
int mid = left + ((right - left) / 2);//等价于(left + right)/2,防止溢出
if(nums[mid]==target) return mid;
if(nums[mid]>target) right = mid - 1;
else left = mid + 1;
}
return -1;
}
};
剑指 Offer 53 - I. 在排序数组中查找数字 I(简单难度)
class Solution {
public:
/*解析:本题是在一个排序数组中查找,首选二分查找法。
本题的难点在于数组中存在重复元素,传统的二分查找法可以找到其中某一个target,但不能判断是哪一个
可以巧妙地在传统二分查找得到的index上试着左移或右移来找到边界。
*/
int search(vector<int>& nums, int target) {
int index=binarySearch(nums,target);//尝试二分查找
if(index==-1) return 0;// 数组中不存在target
// 此时数组中存在target,但是不确定有几个target,index指向其中的一个target
// 寻找左边界,尝试多次移动左边界
int leftBorder=index;
while(leftBorder-1>=0&&nums[leftBorder-1]==target)leftBorder--;
// 寻找右边界,尝试多次移动右边界
int rightBorder=index;
while(rightBorder+1<nums.size()&&nums[rightBorder+1]==target)rightBorder++;
return rightBorder-leftBorder+1;
}
// 二分查找函数[left,right]
int binarySearch(vector<int>& nums, int target) {
int left = 0;
int right = nums.size()-1;
int res = nums.size(); // 记录一下leftBorder没有被赋值的情况
while (left <= right) {
int mid = left + ((right - left) / 2);//等价于(left + right)/2,防止溢出
if(nums[mid]==target) return mid;
if(nums[mid]>target) right = mid - 1;
else left = mid + 1;
}
return -1;
}
};
四、二分查找旋转排序数组中的最小值
153. 寻找旋转排序数组中的最小值(中等难度)
154. 寻找旋转排序数组中的最小值 II(困难难度)
剑指 Offer 11. 旋转数组的最小数字(简单难度)
class Solution {
public:
/*本题是求一个有重复元素的旋转排序数组中的最小值,
实际上无论是否旋转,数组的最小值都是不变的,所以通过这道题很简单。
由于限定算法的时间复杂度为O(logn),所以必须使用二分查找法。
假设数组最后1个元素为nums[right],
则最小值的右侧元素都<=nums[right];最小值的左侧元素都>=nums[right];
我们的nums[mid]可以和nums[right]比较来进行二分查找
有没有重复元素会影响nums[mid]==nums[right]的情况:
没有重复元素时,mid一定就是right,right==mid即可,当然right-=1也对。
有重复元素时,mid可能在最小值右侧或者左侧,索性采取保守策略,right统一左移1个元素。
*/
int findMin(vector<int>& nums) {
int left = 0;// 左指针
int right = nums.size() - 1;//右指针
// 循环遍历[left,right]
while (left < right) {
int mid = left + (right - left) / 2;//折半操作
// 如果nums[mid]==nums[right],说明mid就是右指针,右指针左移1次
if(nums[mid]==nums[right]) right -= 1;//无重复元素时right = mid也可以
// 如果nums[mid]<nums[right],说明mid在最小值右侧,移动右指针
else if (nums[mid] < nums[right]) right = mid;
// 如果nums[mid]>nums[right],说明mid在最小值左侧,移动左指针
else left = mid + 1;
}
// 遍历结束时,left==right,指向同一个元素,就是最小值
return nums[left];
}
};
五、二分查找旋转排序数组中的某个值
33. 搜索旋转排序数组(中等难度)
class Solution {
public:
/* 本题的题意中提到了排序以及时间复杂度为O(log n),所以在暗示你使用二分查找法。
本题是在一个局部有序的数组中查找某个值,原先升序数组经过旋转变成了局部有序,增加了复杂度
本题的旋转数组有两个升序区间。
二分查找法最重要的明白nums[mid]和谁比较,本题整体是nums[mid]和target比较,存在三种情况:
nums[mid]和nums[0]的比较结果会影响上述三种情况的判断。*/
int search(vector<int>& nums, int target) {
int n = nums.size();
if (n==0) return -1;
if (n==1) return nums[0] == target ? 0 : -1;
int left = 0;// 左指针
int right = n - 1;// 右指针
// 二分查找[left,right]
while (left <= right) {
int mid = right+(left-right) / 2;
// 如果nums[mid] == target,已找到,直接返回
if (nums[mid] == target) return mid;
// 如果nums[mid] >=nums[0],说明mid在第一段升序区间中,直接返回
if ( nums[mid]>=nums[0] ) {
if (nums[0] <= target && target < nums[mid]) right = mid - 1;
else left = mid + 1;
}
// 如果nums[mid] <nums[0],说明mid在第二段升序区间中,
else {
// 如果nums[mid]< target并且target没有越界,移动左指针
if (nums[mid] < target && target <= nums[n - 1]) left = mid + 1;
// 如果nums[mid]>= target,移动右指针
else right = mid - 1;
}
}
return -1;
}
};
81. 搜索旋转排序数组 II(中等难度)
class Solution {
public:
bool search(vector<int>& nums, int target) {
int n = nums.size();
if (n==0) return false;
if (n==1) return nums[0] == target;
int left = 0;// 左指针
int right = n - 1;// 右指针
// 二分查找[left,right]
while (left <= right) {
int mid = right+(left-right) / 2;
// 如果nums[mid] == target,已找到,直接返回
if (nums[mid] == target) return true;
if(nums[left] == nums[mid] && nums[mid] == nums[right]){
left++;
right--;
}
// 如果nums[mid] >=nums[0],说明mid在第一段升序区间中,直接返回
else if ( nums[mid]>=nums[left] ) {
if (nums[left] <=target && target < nums[mid]) right = mid - 1;
else left = mid + 1;
}
// 如果nums[mid] <nums[0],说明mid在第二段升序区间中,
else {
// 如果nums[mid]< target并且target没有越界,移动左指针
if (nums[mid] < target && target <= nums[n - 1]) left = mid + 1;
// 如果nums[mid]>= target,移动右指针
else right = mid - 1;
}
}
return false;
}
};
面试题 10.03. 搜索旋转数组
class Solution {
public:
/*该题的难点在于首尾可能存在相同的数,从而不具备二段性*/
int search1(vector<int>& arr, int target) {
int left=0, right=arr.size()-1;
// 恢复二段性
while(right!=0 && arr[0]==arr[right]) right--;
//找到旋转点
while(left<right){
int mid=(left+right+1)/2;
if(arr[mid]>=arr[0]) left=mid;
else right=mid-1;
}
//确定搜索片段
if(target>=arr[0]){
left=0;
right=right;
}
else{
left++;
right=arr.size()-1;
}
//二分搜索
while(left<=right){
int mid=(left+right)/2;
if(target==arr[mid]){
//找到最左边的点
if(mid>0){
if(arr[mid-1]!=target){
return mid;
}
else{
right=mid-1;
continue;
}
}
else return mid;
}
else if(target<arr[mid])right=mid-1;
else left=mid+1;
}
return -1;
}
};
六、有序数组中的单一元素
540. 有序数组中的单一元素
class Solution {
public:
/*该题可以利用索引的奇偶性来进行二分查找,假设目标索引是x
对于下标x左边的下标y,如果nums[y]=nums[y+1],则y一定是偶数;
对于下标x右边的下标z,如果nums[z]=nums[z+1],则z一定是奇数。
下标x是相同元素的开始下标的奇偶性的分界*/
int singleNonDuplicate(vector<int>& nums) {
int left=0;
int right=nums.size()-1;
while(left<=right){
int mid = left+(right-left)/2;
// 查找区间只剩一个元素,就是答案
if(left==right) return nums[mid];
// 如果索引是奇数
mid-=mid&1;//如果是奇数,mid&1=1
if(nums[mid]==nums[mid+1]) left=mid+2;
else right = mid;
}
return -1;
}
};
七、按值二分
287. 寻找重复数
class Solution {
public:
/*cnt[i]表示nums数组中<=i的个数,比如:
[1,2,3,4,4,5,6,7] nums
[0,1,2,3,4,5,6,7] 下标
[0,1,2,3,5,6,7,8] cnt[i]
观察cnt[i]和下标的值得出结论:
当 i < target 时,cnt[i]=i
当 i >= target 时,cnt[i]>i*/
int findDuplicate(vector<int>& nums) {
int n = nums.size();
int left=1, right = n-1, res = -1;
// 二分查找[left,right]
while (left<=right) {
int mid = right+(left-right)/2;
// 遍历整个数组
int cnt = 0;//数组中小于等于mid的个数
for (int i=0;i<n;++i) {
if(nums[i]<=mid) cnt++;
}
if(cnt<=mid)left=mid+1;
else {
right = mid-1;
res = mid;
}
}
return res;
}
};
4. 寻找两个正序数组的中位数(困难难度)
class Solution {
public:
/*中位数是有序数组中可以把所有元素左右等分的一个元素,
如果数组元素是奇数个,正好是中间位置的元素;
如果数组元素是偶数个,则是中间2个元素的均值;
一个自然的思路是:合并两个有序数组为一个有序数组,然后通过有序数组的下标计算中间位置
该算法的思路是O(m+n)*/
double findMedianSortedArrays1(vector<int>& nums1, vector<int>& nums2) {
int m=nums1.size();
int n=nums2.size();
int len=m+n;
vector<int> res(len);
int i=0,j=0,k=0;
while(i<m&&j<n){
if(nums1[i]<=nums2[j]) res[k++]=nums1[i++];
else res[k++]=nums2[j++];
}
while(i<m) res[k++]=nums1[i++];
while(j<n) res[k++]=nums2[j++];
// 一种统一处理中位数的技巧
int left=(len+1)/2;
int right=(len+2)/2;
// 从两个有序数组中查找第left大和第right大的数
double mean=(res[left-1]+res[right-1])/2.0;
return mean;
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size();
int n = nums2.size();
// 想要寻找的位置
int left=(m+n+1)/2;
int right=(m+n+2)/2;
// 从两个有序数组中查找第left大和第right大的数
double mean=(findKth(nums1,nums2,0,0,left)+findKth(nums1,nums2,0,0, right))/2.0;
return mean;
}
// 从nums1的[i:]和nums2[j:]的两个有序区间中查找第k大的数
int findKth(vector<int>& nums1, vector<int>& nums2,int i, int j, int k){
// 终止条件
if(i>=nums1.size()) return nums2[j+k-1];//nums1为空数组,直接从nums2中找
if(j>=nums2.size()) return nums1[i+k-1];//nums2为空数组,直接从nums1中找
if(k==1) return min(nums1[i], nums2[j]);// 查找第1个元素,直接返回
// 查找nums1中的第k/2大元素,不存在则设为无穷大
int midVal1=(i+k/2-1<nums1.size())? nums1[i+k/2-1] : INT_MAX;
// 查找nums2中的第k/2大元素,不存在则设为无穷大
int midVal2=(j+k/2-1<nums2.size())? nums2[j+k/2-1] : INT_MAX;
// 如果midVal1<midVal2,说明中位数肯定不在nums1的前k/2个数
if(midVal1<midVal2) return findKth(nums1, nums2, i+k/2,j, k-k/2);
// 如果midVal1>=midVal2,说明中位数肯定不在nums2的前k/2个数
else return findKth(nums1, nums2, i,j+k/2 , k-k/2);
}
};