二分查找场景:有序数组寻找一个数、寻找左侧边界(有序数组第一个等目标数的下标)、寻找右侧边界(有序数组最后一个等于目标数的下标)
1 二分查找的框架
int binarySearch(int *nums,int numsSize,int target)
{
int left=0,rigth = ...;
while(...)
{
int mid = left +(rigth-left)/2;
if(nums[mid] == target)
{
...
}else if (nums[mid]<target)
{
left = ...;
}
else if (nums[mid]>target)
{
rigth = ...;
}
}
return ...
}
使用else if是为了把所有情况写清楚,这样可以清楚的展现所有细节。本文都使用else if,旨在说清楚,可自行简化。
其中…标记的部分,就是可能出现细节问题的地方,当你见到一个二分查找的代码时,首先要注意这几个地方。
2 寻找一个数(基本的二分搜索)
这个场景是最简单的,即搜索一个数,如果存在,返回其索引,否则返回-1。
int binarySearch(int *nums,int numsSize,int target)
{
int left=0;
int right = numsSize-1; //注意
while(left<=rigth) //注意
{
int mid = left +(right-left)/2;
if(nums[mid] == target)
return mid;
else if(nums[mid]<target)
{
left = mid+1; //注意
}
else if(nums[mid]>target)
{
right = mid+1; //注意
}
}
return -1;
}
- 为什么while的循环条件中是<=,而不是<?
答:因为初始化right的赋值是numsSize-1,即最后一个元素的索引,而不是numsSize,这二者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间[left,right],后者相当于左闭右开区间[left,right),因为索引大小为numsSize是越界的。
我们这个算法中使用的是[left,right]两端都闭的区间,这个区间就是每次进行搜索的区间,那while循环什么时候应该终止?搜索区间为空的时候应该终止,意味着你没的找了。
while(left<=right)终止条件是left==right+1,写成区间的形式是[ right+1,right],这个时候搜索区间为空,直接返回-1.
while(left<right)的终止条件是left==right,写成区间的形式是[right,right],者时候区间非空,还有一个数nums[right],如果此时while循环停止了,就漏掉一个数,如果这个时候返回-1就可能出现错误。
- 为什么left=mid+1,right=mid-1?
本算法的搜索区间两端都是闭的,即[left,right]。那么当我们发现索引mid不是要找的target时,如果确定下一步的搜索区间呢?
当然是去搜索[left,mid+1]或者[mid+1,right],因为mid已经搜索过了,应该从搜索区间去除。 - 此算法有什么缺陷?
比如说你有有序数组nums=[1,2,2,2,3],此算法返回的索引是2,但是如果我想得到target的左侧边界,即索引1,或者想得到target的右侧边界,即索引3,这样的话,此算法是无法处理的。
3 寻找左侧边界的二分搜索
查找左侧边界的数,即在一个有序数组中,找到第一个等于target的下标。比如数组int nums[]={5,7,7,8,8,8,10},target=8,第一个等于8的下标是3,第一个大于等于8的数组下标也是3。即找到第一个等于target的数等价于 找第一个大于等于targte的数的下标,然后我们判断该下标所对应的数是否是target。
直接看代码:
/* 二分查找左侧边界 */
int lower_bound(int *nums,int numsSize,int target)
{
int left=0,right=numsSize-1,ans=numsSize;
while(left<=right)
{
int mid = left+(right-left)/2;
if(nums[mid]>=target)
{
right = mid-1;
ans = mid;
}
else
{
left = mid+1;
}
}
return ans;
}
int main(void)
{
int nums[]={5,7,7,8,8,8,10};
int ret;
//int p = high_bound(nums,7,8);
//printf("%d \n",p);
ret = lower_bound(nums,7,8);
printf("%d \n",ret);
return 0;
}
输出是3。
当nums[mid]>=target
时,说明有大于等于target的数了,我们需要更新ans来记录大于等于targte的数,right需要更新,然后继续往在[left,mid-1]区间找大于等于target的数,如果nums[mid]<target
,则需要在[mid+1,right]区间找大于等于target的数,left下标所指向的值始终是小于等于target(如果全部数据全部大于target,left将不会变化),所以,当结束时,ans所指向的值是[left,right]区间最后一个大于等于target的值,left下标所指向的值又始终是小于等于target,所以ans所指向的内容是第一个大于等于target的值。
4 寻找右侧边界的二分查找
寻找右侧边界的数,就是找第一个大于target的数,返回其下标减1,int nums[]={5,7,7,8,8,8,10},最后一个等于8的下标是5,第一个大于8的数是10,其下标是6,减去1是5,找target最后位置等价于找第一个大于target的下标减1,然后判断该位置上的数是否等于target。
具体代码为:
/* 二分查找右侧边界 */
int high_bound(int *nums,int numsSize,int target)
{
int left=0,right=numsSize-1,ans=numsSize;
while(left<=right)
{
int mid = left+(right-left)/2;
if(nums[mid]>target)
{
right = mid-1;
ans = mid;
}
else
{
left = mid+1;
}
}
return ans;
}
int main(void)
{
int nums[]={5,7,7,8,8,8,10};
int ret;
//int p = high_bound(nums,7,8);
//printf("%d \n",p);
ret = high_bound(nums,7,8)-1;
printf("%d \n",ret);
return 0;
}
运行结果是5。
如果nums[mid]>target,有大于target的数了,ans就要去记录,right更新,right=mid-1 ,如果nums[mid]<=target,则left需要更新,left=mid-1,left指示的内容始终是小于等于target的(如果数据全部大于target,left不会变)。只要left<=right,就会一直缩小区间,当运行结束后,ans所指示的内容是最后一个大于target的数。
5 合并
我们添加一个参数,表示找第一个等于target的数,还是找最后一个等于target的数。
int binarySearch(int* nums, int numsSize, int target, bool lower) {
int left = 0, right = numsSize - 1, ans = numsSize;
while (left <= right) {
int mid = (left + right) / 2;
if (nums[mid] > target || (lower && nums[mid] >= target)) {
right = mid - 1;
ans = mid;
} else {
left = mid + 1;
}
}
return ans;
}
当lower为真时,表示找第一个大于等于target的数,当lowe为假时,表示找第一个大于target的数,返回之后,检查和上面代码一样。
leedcode的第34题:在排序数组中查找元素的第一个和最后一个位置
示例代码:
/*
在一个升序数组中,找第一个等于target的数组,即找第一个大于等于target的数,返回其下标,最后判断
是否等于targettarget。
找最后一个等于target的数组,即找第一个大于target的数,返回其下标减1,最后判断该下标对应的数是否等于target。
*/
int binarySearch(int *nums,int numsSize,int target,bool lower)
{
int left=0,right=numsSize-1,ans=numsSize;
while(left<=right)
{
int mid=left+(right-left)/2;
if(nums[mid]>target || (lower && nums[mid]>=target))
{
right=mid-1;
ans = mid;
}
else
{
left=mid+1;
}
}
return ans;
}
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* searchRange(int* nums, int numsSize, int target, int* returnSize){
int leftIdx = binarySearch(nums,numsSize,target,true);
int rightIdx = binarySearch(nums,numsSize,target,false)-1;
int *ret = (int *)malloc(sizeof(int)*2);
*returnSize=2;
if(leftIdx<=rightIdx && nums[leftIdx]==target && nums[rightIdx]==target)
{
ret[0]=leftIdx;
ret[1]=rightIdx;
return ret;
}
ret[0]=-1;
ret[1]=-1;
return ret;
}