基本的二分查找、寻找第一个和最后一个数的二分查找


二分查找场景:有序数组寻找一个数、寻找左侧边界(有序数组第一个等目标数的下标)、寻找右侧边界(有序数组最后一个等于目标数的下标)

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;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值