前言:判断一道题是否能用二分,可以根据这道题是否有二段性来区分
简单模板
通过题目向大家讲解二分算法
链接: https://leetcode.cn/problems/binary-search/description/
这是一个很简单的二分查找的题目。那么接下来我们就通过题目来理解二分算法。
题意很简单:在一个升序的数组中查找目标值target,如果找到target就返回该值的下标,找不到就返回-1。
常规的做法:常规做法就是遍历一遍数组,如果遇到值为target的数,返回其下标,否则在遍历完之后返回-1 时间复杂度O(n)
不过常规做法没有考虑到数组的特性
升序
更新方式
按照示例一给的数组
[-1,0,3,5,9,12] target = 9
如果我们定义一个参数mid表示数组的中间索引(下标),在本例中left = 0, right = n - 1 = 5,mid = (left + right) / 2 = 2
nums[mid]就为3,此时我们判断,nums[mid] < target,并且由于数组是升序的,得出 --> nums[mid]之前的数也肯定小于target,所以可以直接不考虑了。
根据之前的判断,就可以直接考虑mid + 1后面的数了,此时就可以做一个更新,更新区间。left = mid + 1, right不变。
按照示例二给的数组
[-1,0,3,5,9,12] target = 2
定义一个参数mid表示数组的中间索引(下标),在本例中left = 0, right = n - 1 = 5, mid = 2
nums[mid]为3,nums[mid] > target,由于数组是升序,得出–> nums[mid]后的数也肯定大于target,所以可以直接不考虑了。
根据之前的判断,就可以直接考虑mid - 1后面的数了,此时就可以做一个更新,更新区间。right = mid - 1, left不变。
循环条件
定义变量为left = 0,right = n - 1,也就是为数组的两边,为了方便遍历。
- left < right的时候,可以继续循环
- left == right 的时候,是否也满足循环的条件呢?
存在一种可能,当left = right的时候,正好就是答案,所以left = right也属于循环条件
根据上述的写代码:
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while(left <= right)
{
int mid = left + (right - left) / 2;
if(nums[mid] < target) left = mid + 1;
else if (nums[mid] > target) right = mid - 1;
else return mid;
}
return -1;
}
};
需要特别解释的是mid = left + (right - left) / 2,
之所以不使用mid = (left + right) / 2是因为,可能加法可能会导致溢出,所以使用减法。
right - left是两指针之间的距离,(right - left) / 2就是left到mid之间的距离,加上left表示 0 到mid之间之间的距离
总结模板
while(left <= right)
{
int mid = left + (right - left) / 2;
if(……) left = mid + 1;
else if (……) right = mid - 1;
else return mid;
}
进阶模板
链接: https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/description/
题意理解:在一个非递减的数组中找到目标值的左端点和右端点。如果目标值不存在就返回[-1,-1],如果数组为空也返回[-1,-1]
此题可以很清楚的分为两部分:查找左端点和查找右端点。
查找左端点
从上图中可以看出,我们可以将数组分为两部分,一部分是小于t,另一部分是大于等于t
可以将数组分为两部分是由数组的性质 ---- 非递减得到的
1.循环条件
left < right
为什么left <= right不行?
2.更新方式
根据之前将数组分为两部分:小于t,大于等于t
定义一个变量mid,表示left 和 right的中间值
- 如果mid处于小于t的区间,那么表达式是什么呢?
nums[mid] < t
当mid处于小于t的区间,而我们要寻找的内容是等于t的数,所以mid所处的区间显然不是正确的区间(mid所指向的值不可能正好等于t,所以可以将mid去除),我们就要进行优化
优化的方式就是将mid换到另一个区间里,那就是left = mid + 1
-
如果mid处于大于等于t的区间,那么表达式是什么呢?
nums[mid] >= t当mid处于大于等于t的区间,而我们要寻找的内容是等于t的数,所以mid所处的区间有可能是正确的区间(mid所指向的值有可能正好等于t,所以不能将mid去除)
优化方式是right = mid
3.求中点的方式
求中点有两种方式:
- mid = left + (right - left) / 2
- mid = left + (right - left + 1) / 2
两种方式在数字个数是奇数的时候没有什么区别,在偶数的时候有区别,不过对于解题没有什么影响。
但是,如果在数组中只有两个数的时候,这个影响就会非常大!!!
因为我们的更新方式是right = mid,但是在求中点的时候,如果使用mid = left + (right - left + 1) / 2,就会导致mid向上取整,mid = right
right = mid, mid = right导致两个值都不能改变,最后导致死循环
查找右端点
从上图中可以看出,我们可以将数组分为两部分,一部分是小于等于t,另一部分是大于t
1.循环条件
left < right
理由同查找左端点。
2.更新方式
根据之前将数组分为两部分:小于等于t,大于t
定义一个变量mid,表示left 和 right的中间值
- 如果mid处于小于等于t的区间,那么表达式是什么呢?
nums[mid] <= t
当mid处于小于等于t的区间,而我们要寻找的内容是等于t的数,所以mid所处的区间有可能是正确的区间(mid所指向的值有可能正好等于t,所以不能将mid去除)
优化的方式就是将mid换到另一个区间里,那就是left = mid
-
如果mid处于大于t的区间,那么表达式是什么呢?
nums[mid] > t当mid处于大于t的区间,而我们要寻找的内容是等于t的数,所以mid所处的区间显然不是正确的区间(mid所指向的值不可能正好等于t,所以可以将mid去除),我们就要进行优化
优化方式是right = mid - 1
3.求中点的方式
求中点有两种方式:
- mid = left + (right - left) / 2
- mid = left + (right - left + 1) / 2
同样的理由,因为更新的方式是left = mid,所以如果采用向下取整,在数组剩最后两个数的时候,就会导致left = mid,mid = left,最后进入死循环。
总结
1.如何确定更新方式?
当我们判断更新的区间,如果mid在这个区间内有可能是答案,那么就是left = mid或者right = mid
如果mid在这个区间内不可能是答案,那么就是left = mid + 1,right = mid - 1
2.如果确定求中点的方式?
规律:当出现right = mid - 1的时候,求中点的方式就需要+1
当算法的原理讲解完之后,做题就比较简单
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
//1.处理数组为空的情况
if (nums.size() == 0) return {-1,-1};
//2.查找左端点
int left = 0, right = nums.size() - 1, begin = 0;
while(left < right)
{
int mid = left + (right - left) / 2;
if (nums[mid] < target) left = mid + 1;
else right = mid;
}
//3.特殊情况处理 -- left和right的值不是targert就表示不存在
if(nums[left] != target) return {-1,-1};
//4.查找右端点
begin = left, left = 0, right = nums.size() - 1;
while(left < right)
{
int mid = left + (right - left + 1) / 2;
if (nums[mid] > target) right = mid - 1;
else left = mid;
}
return {begin, right};
}
};