二分查找
二分查找是一种针对有序线性表的高效查找算法,在查找序列有序时,我们使用顺序查找和使用线性查找在数据规模较大的时候会有比较大的效率差别,很多人觉得二分就是整个左右指针往中间怼,就能达成目的,但是二分的细节还是蛮多的。
常规二分
对于常规的二分查找,一般是在指定的有序队列(无序如果有二分的需求的话就先排个序)中查找目标值,这个是二分最简单,也是最多为人所知的用法。
下面我们来看一下二分查找的代码:
#include <iostream>
#include <vector>
int binarySearch(vector<int> nums, int target){
int left = 0,right = nums.size();
while(left < right){
int mid = (left + right)/2;
if(nums[mid] == target){
return mid;
}else if(nums[mid] < target){
left = mid + 1;
}else if(nums[mid] > target){
right = mid;
}
}
return -1;
}
在这段代码里,有一个可以优化的地方,如果left + right > INT_MAX,那么我们的结果将会溢出,所以,我们可以写成这个样子
int mid = left + (right - left)/2;
还可以进一步写成
int mid = left + (right - left) >> 1;
这样就能防止隐藏的溢出错误了。
除此之外,可以看到我们使用的right = nums.size(); while的判别条件也是left < right,这是因为right是实际不可取的下表范围,而while的停止条件是left >= right,要是while的判别条件是left <= right,而且要查找的数是最后一个数的话,程序是会产生下标越界错误的。(相当于说我们划分的范围在[left,right)之间,区间左闭右开)。
总结起来就是:
判别条件为:“left < right” -> right应该取nums.size();
判别条件为:“left <= right” -> right应该取nums.size() - 1;
再说说为什么我们在判断条件中选择使用
if(nums[mid] == target){
return mid;
}else if(nums[mid] < target){
left = mid + 1;
}else if(nums[mid] > target){
right = mid;
}
– 在nums[mid] == target时,说明我们找到了,直接返回该下标。
– 在nums[mid] < target时,说明target在mid的右边,所以我们可以把区间收缩到[mid + 1,right),mid已经用过了。注意左闭右开,这是由上面while的判别条件(也是由我们right的取值)决定的。
– 同理,在nums[mid] > target时,我们把区间收缩到[left,mid)。
– 在有序区间没找到的话,那就返回-1。
查找左侧边界的二分查找
先上代码:
int B_S_lower_bound(vector<int> nums, int target) {
if(nums.size() == 0){
return -1;
}
int left = 0,right = nums.size();
while(left < right) {
int mid = left + (right - left) >> 1;
if(nums[mid] == target){
right = mid;
}else if(nums[mid] < target){ //锁定左边界
left = mid + 1;
}else if (nums[mid] > target)
right = mid;
}
return left;
}
我们来分析一下里面的判断条件
– 当nums[mid] == target时,我们可以确定,mid的右边一定不是target的左边界所在的地方,所以我们把区间收缩成[left,mid);
– 当nums[mid] < target时,target的左边界一定在mid左边,因为mid已经使用过了,所以我们把区间收缩到[mid + 1,right);
– 当nums[mid] > target时,和nums[mid] == target同理,把区间收缩成[left,mid);
最终我们返回的是left,其实返回left和right都可以,因为终止条件是left == right;
查找右侧边界的二分查找
先上代码:
int B_S_upper_bound(vector<int> nums, int target) {
if(nums.size() == 0){
return -1;
}
int left = 0,right = nums.size();
while(left < right) {
int mid = left + (right - left) >> 1;
if(nums[mid] == target){
left = mid + 1;
}else if(nums[mid] < target){
left = mid + 1; //锁定右边界
}else if (nums[mid] > target){
right = mid;
}
}
return nums[left - 1] == target ? left - 1:left;
}
我们来分析一下里面的判断条件
– 当nums[mid] == target时,我们可以确定,mid的左边一定不是target的右边界所在的地方,所以我们把区间收缩成[mid + 1,right);
– 当nums[mid] < target时,同上,我们把区间收缩到[mid + 1,right);
– 当nums[mid] > target时,我们可以知道,target的右边界一定不会在右边,所以我们把区间收缩成[left,mid);
最终我们返回的是一个三元算式,本来是看着写的 return left - 1;
left或right无所谓,-1主要是因为我们收缩右边界时都会把mid抛弃,但实际可能mid就是target的右边界。
我自己进行修改主要是因为当我自己测试
return left - 1;
}
vector<int> array = {1,3,4,5,5,7,9,10};
cout << B_S_lower_bound(array,5);
cout << B_S_upper_bound(array,6);
的时候,它输出的是3 4
个人认为5作为6的右边界是反直觉的.所以改成这样。(如果这样是错的劳请各位批评指正)
参考的大佬的文章:
我作了首诗,保你闭着眼睛也能写对二分查找
大佬的公众号:labuladong