-----------------------------------------------------二刷2021/2/4----------------------------------------------------
这几天一天刷十题,很快啊,现在已经是第六天了,前面35题三刷过了,马上后面也要做完了
总的来说该写的之前都写了。
实实在在觉得菜鸡JL有点脱胎换骨了。还有一个月就来到金三了!加油!
二分法太不熟悉了
解法 二分法
在本题的二分开始之前,先来看看二分的两种方法
- 第一种 左闭右闭区间 [L , R]
public int binarySearch(int arr[], int target){
if (arr!=null&&arr.length>0){
// 注意 l 与 r 的初值
int l=0,r=arr.length-1;//定义 在[l...r]的范围里寻找target,循环不变量,虽然在下面修改了l与r的值,但是其语义不变,
//只要还有查找内容的话,就要继续查找
//当l==r 时,区间[l...r]依然是有效的,也就是还有内容要查找
//所以是 l<=r 而不是l<r
while (l<=r){
int mid=l+(r-l)/2;//防溢出
if (arr[mid]==target){
return mid;
}else if (target>arr[mid]){
l=mid+1;//target在[mid+1...r]中,因为已经明确mid位置及其左边的值都不是目标值
}else {
r=mid-1;//target在[l...mid-1]中,因为已经明确mid位置及其右边的值都不是目标值
}
}
}
return -1;
}
- 第二种 左闭右开区间[L , R)
public int binarySearch2(int arr[], int target){
if (arr!=null&&arr.length>0){
// 注意 l 与 r 的初值
int l=0,r=arr.length;//定义 在[l...r)的范围里寻找target
while (l<r){//当l==r 时,区间[l...r)是无效的,[42,42) ,所以这里是 小于 而不用小于等于
int mid=l+(r-l)/2;
if (arr[mid]==target){
return mid;
}else if (target>arr[mid]){
l=mid+1;//target在[mid+1...r)中,因为mid位置及其左边的值都不是目标值
}else {
r=mid;//target在[l...mid)中 ,因为已经明确mid位置及其右边的值都不是目标值,这里mid是右开的,所以是取不到的
}
}
}
return -1;
}
注释里面都写得很清楚了,推荐是用第一种方法
题目要求我们找出指定数字的出现次数,我们使用二分法查找该数字的左边界和右边界
答案即是 right - left - 1
普通的二分法只能让我们停留在target的位置,但是鉴于有很多target我们需要修改arr[mid] == target处理方式。
当arr[mid] == target时
1. 若我们要查找右边界,则执行 l = m + 1
循环结束的时候l会停在右边界,r会停留在l-1
2. 若我们要查找左边界,则执行 r = m - 1
循环结束的时候,r会停留左边界,l会停留在 r + 1
这里有个很关键的地方,如果数组中确实存在target时,查找右边界以后,arr[r]就为target。
而同理,查找左边界以后,arr[l]为target。
那么为什么查找右边界的时候 r会停在最右边的target,而查找左边界的时候l会停在最左边的target 是二分最难的地方
分析二分的过程就能知道原因
我们在二分时保证target一定处于区间内,所以这个区间包括了所有的target,直到出现arr[mid] = target。这个时候就必须开始舍弃一部分的target。
1. 若我们要查找右边界,则执行 l = m + 1
- 这样左边的target将会一直减少,当l一直移动到r的位置时,mid和r同时处在最后一个target的下一个位置,这时再执行r = m - 1就会使得 l = r + 1,循环终止
- 或者 r可能在循环的过程中由于出现过 arr[mid] > target,且mid刚好是右边界,这时候r = m - 1就会停留在最后一个target的位置等待l与其相交 ,相交之后执行 l = m +1使得 l = r + 1,循环终止
2. 若我们要查找左边界,则执行 r = m - 1
- 这样右边的target将会一直减少,当r 一直移动到 l的位置,mid 和l同时处在第一个target的上一个位置,这时再执行 l = mid + 1就会使得 l = r + 1.循环结束
- 或者同理 l可能在循环的过程中出现过arr[mid] < target ,其mid刚好是左边界,这时候 l = m + 1就会停留在第一个target的位置等待r与其相交。相交之后执行r = m - 1,使得 l = r + 1,循环终止
总之查找右边界的过程中,r一定不会越过最右边的target ,同理查找左边界的过程中,l一定不会越过最左边的target。
通过上诉分析可见其逻辑以及正确性。即 结束时 l == r + 1
具体来分析一个案例
5 7 7 8 8 9 target = 8
- 查找右边界
l = 0 r = 5 mid = 2
arr[mid]=7 < target l =mid + 1
l = 3 r = 5 mid = 4
arr[mid]=8 = target l = mid + 1
l = 5 r = 5 mid = 5
这时候我们的l已经处于左边界了,但区间内仍有数字所以循环还在继续
arr[mid]= 9 > target r = mid - 1
l = 5 r = 4 退出循环
arr[l] = 9 arr[r] = 8
还有其他情况留给读者自己分析。
代码:
class Solution {
public:
int search(vector<int>& nums, int target) {
if(nums.size() == 0) return 0;
//查找右边界
int l = 0, r = nums.size() - 1;
while(l <= r) {
int mid = (l + r) >> 1;
//mid小于等于target,即mid在target左边 说明target在[m + 1, j]
if(nums[mid] <= target) l = mid + 1;
//mid大于target,即mid在target右边 说明target在[l, m -1]
else r = mid - 1;
}
//这轮结束的时候,l会停在右边界,r会停留在l-1,如果r < 0或者
//如果nums[r] != target 说明不存在target
if(r < 0 || nums[r] != target) return 0;
int right = l;
//r停留在最后一个target的位置无需改变
l = 0;
while(l <= r) {
int mid = (l + r) >> 1;
//mid大于等于target,即mid在target右边 说明target在[l, m -1]
if(nums[mid] >= target) r = mid - 1;
//mid小于target,即mid在target左边 说明target在[m + 1,r]
else l = mid + 1;
}
//这轮结束的时候,r会停留左边界,l会停留在 r + 1
int left = r;
return right - left - 1;
}
};
时间复杂度O(logN)
空间复杂度O(1)