概要
曾经在一篇简单分治文章中讨论过二分查找边界问题的两种解决方法,但是昨天我看到一个让我耳目一新的二分查找边界问题解决方法,它的关键就是抓住循环不变量。
循环不变量是什么?这个不加以详细诉说,我们直入主题,二分查找如何通过循环不变量来解决边界问题。
循环不变量规则
二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 while(left < right)
还是 while(left <= right)
,到底是right = middle
呢,还是要right = middle - 1
呢?
大家写二分法经常写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
二分的两种写法
写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
第一种写法
第一种写法,我们定义 target 是在一个在左闭右闭的区间里,**也就是[l, r] **。
我们对区间的定义这就决定了二分法的代码应该如何写,因为定义target在[l, r区间,所以有如下两点:
-
while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
-
if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
所以我们的代码可以这么写:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,所以[middle + 1, right]
} else { // nums[middle] == target
return middle; // 数组中找到目标值,直接返回下标
}
}
// 未找到目标值
return -1;
}
第二种写法
如果说定义 target 是在一个在左闭右开的区间里,也就是**[left, right)** ,那么二分法的边界处理方式则截然不同。有如下两点:
-
while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
-
if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]
代码入下:
i
nt search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle; // target 在左区间,在[left, middle)中
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,在[middle + 1, right)中
} else { // nums[middle] == target
return middle; // 数组中找到目标值,直接返回下标
}
}
// 未找到目标值
return -1;
letcode一些二分练习
第一题
分析:无非就是三个情况
- 数组前插入
- 数组中插入
- 数组后插入
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int l=0,r=nums.size()-1,mid;
while(l<=r)
{
mid=l+(r-l)/2;
if(nums[mid]>target)
{
r=mid-1;
}
else if(nums[mid]<target)
{
l=mid+1;
}
else{
return mid;
}
}
return r+1;//最后r一定小于l,若在r和l相等时,nums[mid]>target,r=mid-1,那么插入位置在r右侧,反之,nums[mid]<target,插入位置也在r右侧。
}
};
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int l=0,r=nums.size(),mid;
while(l<r)
{
mid=l+(r-l)/2;
if(nums[mid]>target)
{
r=mid;
}
else if(nums[mid]<target)
{
l=mid+1;
}
else{
return mid;
}
}
return r;//分析类似
}
};
第二题
分析:二分查找找到一个位置,若找到,从中心向两边扩展找左边界和右边界,否则返回[-1,-1]
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int min=-1,max=-1;
int l=0,len=nums.size(),r=len-1;
int mid;
while(l<=r)
{
mid=l+(r-l)/2;
if(nums[mid]>target)
r=mid-1;
else if(nums[mid]<target)
l=mid+1;
else
{
min=mid,max=mid;
break;
}
}
vector<int>result;
if(max==-1)
{
result.push_back(-1);
result.push_back(-1);
return result;
}
while((--min)>=0&&nums[min]==target);
while((++max)<len&&nums[max]==target);
result.push_back(min+1);
result.push_back(max-1);
return result;
}
};
第三题
分析:注意下数据,mid*mid可能超出范围,用long long
class Solution {
public:
bool isPerfectSquare(int num) {
long long l=0,r=num,mid;
while(l<=r)
{
mid=l+(r-l)/2;
if(mid*mid==num)
return true;
else if(mid*mid>num)
r=mid-1;
else
l=mid+1;
}
return false;
}
};
第四题
这题官方翻译有问题,上英文版
分析:就把大于小于等于换成-1,0,1罢了
class Solution {
public:
int guessNumber(int n) {
int l=1,r=n;
int mid;
while(l<=r)
{
mid=l+(r-l)/2;
if(guess(mid)==-1)
{
r=mid-1;
}
else if(guess(mid)==1)
{
l=mid+1;
}
else
return mid;
}
return mid;
}
};