目录
二段性:分两段,舍一段,操作另一段(这个是二分查找的本质!)
承接CC53.【C++ Cont】二分查找的普通模版文章继续介绍二分法的另外两个模版,比较重要
1.例题
以LeetCode的34. 在排序数组中查找元素的第一个和最后一个位置题为例:
34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
给你一个按照非递减顺序排列的整数数组
nums
,和一个目标值target
。请你找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值
target
,返回[-1, -1]
。你必须设计并实现时间复杂度为
O(log n)
的算法解决此问题。示例 1:
输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]示例 2:
输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1]示例 3:
输入:nums = [], target = 0 输出:[-1,-1]提示:
0 <= nums.length <= 10^5
-10^9 <= nums[i] <= 10^9
nums
是一个非递减数组-10^9 <= target <= 10^9
分析
理解非递减的含义:值增长或者值不变,以[5,7,7,8,8,10], target = 6示例来画图:
方法1:暴力查找
两个指针left和right分别从左和从右遍历,找到target就退出循环,时间复杂度为
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target)
{
vector<int> ret={-1,-1};
int left=0,right=nums.size()-1;
for (;left<nums.size();left++)
if (nums[left]==target)
{
ret[0]=left;
break;
}
for (;right>=0;right--)
if (nums[right]==target)
{
ret[1]=right;
break;
}
return ret;
}
};
提交结果:
方法2:利用数组有序和二段性来优化
二段性的具体概念参见CC53.【C++ Cont】二分查找的普通模版文章,这里直接给出二段性的总结:
二段性:分两段,舍一段,操作另一段(这个是二分查找的本质!)
找左边界(开始位置)
给定以下数组,设蓝色区域元素的值小于目标值t,绿色区域元素的值大于等于目标值t
如果尝试使用之前CC53.【C++ Cont】二分查找的普通模版文章将的普通二分法的模版来求左边界会有问题,例如设红色区域元素的值都等于目标值t
则mid指向的元素不一定符合左边界的要求
则mid还要向左查找,特殊情况下时间复杂度为,例如[5,5,5,5,5,5,5] target=5
普通二分法需要改进,分两类讨论
1.nums[mid]
t
根据二段性:舍一段,操作另一段
更新left为mid+1
left=mid+1;
2.nums[mid
t
根据二段性:舍一段,操作另一段
更新right为mid
right=mid;
注意不是mid+1,极端情况下当mid正好指向左边界时,更新right为mid-1会错过左边界!!!
细节处理
1.循环条件是left<right
当left=right时,就是最终结果,要么等于目标值t,返回下标,要么不等于目标值t,分类讨论数组的所有情况
情况1:能找到目标值t
通过left和right的移动策略来看:
left=mid+1;
right=mid;
left永远想离开t的区域(因为mid+1),right永远在
t的区域,最终情况一定是left==right,此时left和right都指向左边界
情况2:找不到目标值t,且所有元素大于t
nums[mid]t,right=mid,left不动,最终情况是left==right在区间的最左边,但此时left和right指向的值不等于目标值t
情况3:找不到目标值t,且所有元素小于t
nums[mid]t,left=mid+1,right不动,最终情况是left==right在区间的最右边,但此时left和right指向的值不等于目标值t
2.写left<=right会死循环
情况1中当left=right=左边界时,nums[mid]t,left==right==mid,left和right都不动
3.更新mid的公式
之前在文章中提到过更新mid的公式,有两种写法:
mid=left+(right-left)/2;
mid=left+(right-left+1)/2;
对于查找左边界而言,当left+1==right时,mid=left+(right-left+1)/2==right,当nums[mid],nums[mid]t,right=mid,仍然有mid=right,程序死循环
因此只能使用mid=left+(right-left)/2;
找右边界(结束位置)
和找左边界类似,设蓝色区域元素的值小于等于目标值t,绿色区域元素的值大于目标值t
1.nums[mid]
t
根据二段性:舍一段,操作另一段
left=mid;
2.nums[mid]
t
根据二段性:舍一段,操作另一段
right=mid-1;
循环条件:left<right,更新mid使用mid=left+(right-left+1)/2;,不再证明
代码
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target)
{
vector<int> ret={-1,-1};
if (nums.size()==0)
return ret;
int left=0,right=nums.size()-1;
//能进入循环的前提是数组不为空!!!
while(left<right)
{
int mid=left+(right-left)/2;
if (nums[mid]<target)
left=mid+1;
else
right=mid;
}
if (nums[left]==target)
ret[0]=left;
left=0,right=nums.size()-1;
while(left<right)
{
int mid=left+(right-left+1)/2;
if (nums[mid]<=target)
left=mid;
else
right=mid-1;
}
if (nums[left]==target)
ret[1]=left;
return ret;
}
};
注找完左边界后其实left没有必要置为0,这里因为格式统一写上了
提交结果:
2.查找左边界的二分模版(万能)
//能进入循环的前提是数组不为空!!!
while (left < right)
{
int mid = left + (right - left) / 2;
if (......)
left = mid + 1;
else
right = mid;
}
空缺处填上对应的条件即可
3.查找右边界的二分模版(万能)
//能进入循环的前提是数组不为空!!!
while (left < right)
{
int mid = left + (right - left + 1) / 2;
if (......)
left = mid;
else
right = mid - 1;
}
空缺处填上对应的条件即可
记忆:
分类讨论的代码if (......){...}else{...}就题论题即可