力扣在排序数组中查找元素的第一个和最后一个位置---暴力与二分

34. 在排序数组中查找元素的第一个和最后一个位置

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

进阶:

你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?

示例 1:

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:

输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:

输入:nums = [], target = 0
输出:[-1,-1]

提示:

0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums 是一个非递减数组
-109 <= target <= 109

方法一:暴力搜索

所谓暴力其实就是在数组里挨个寻找target,找到第一个target后储存其下标,接着再次寻找,直到找到最后一个和target相等的数后再次储存其下标即可。

int* searchRange(int* nums, int numsSize, int target, int* returnSize){
    
    
    int *a=(int*)malloc(sizeof(int)*2);//申请空间
    *returnSize = 2;
    memset(a,-1,sizeof(a));//先初始化为-1以便后续操作
    if(numsSize==1)//由后面可知我是先估计了第i个的下一个的情况,所以至少数组长度为2,所以为长度为1要先单独考虑
    {
    
    
        if(target!=nums[0])
        {
    
    
            return a;
        }
        else
        {
    
    
            a[0]=0;
            a[1]=0;
            return a;
        }
    }
    int k = 0;//作为衡量数组中是否出现过与target相等的数
    for(int i=0;i<numsSize;i++)
    {
    
    
        if(nums[i]==target)
        {
    
    
            if(k==0)//此为证明是第一次遇见target
            {
    
    
                a[0]=i;
                k++;//k变了
            }
            if(i!=numsSize-1)//下面的条件都是在下一个的情况考虑的,所以要先保证存在“下一个”
            {
    
    
                if(nums[i+1]==target)
                {
    
    
                    continue;//即判断他是否为最后一个与target相等的数是靠他之后的数是否为target
                }
                if(nums[i+1]!=target)
                {
    
    
                    a[1]=i;
                    break;//找到最后一个立马跳出去
                }
            }
        }
    }
    if(a[1]==-1&&k!=0)//此为找到target但是数组最后一个数也为target的情况
        a[1]=numsSize-1;//这样的话在for循环中不会为a[1]赋值,因为他进不去第二个if了
    if(k==0)//表示没有找到target的情况
    {
    
    
        a[0]=-1;
        a[1]=-1;
        return a;
    }
    return a;
}

但令我没想到的是暴力做法效率还挺快的。
在这里插入图片描述

方法二:二分查找

由题可知,既然已经说了是升序数组,则可利用二分特性查找加快速度。

但与平常的二分有所不同,这里需要找到两个东西,一个是左边界,一个是右边界。先查找左边界,一旦nums[mid]大于或小于target直接常规二分移动即可。重点是当出现nums[mid]==target时,由于此点可能不是左边界,所以要向左再进行查找所以我们的循环结束条件并不是出现nums[mid]==target,而是不断的进行缩小left与right进而使真正的左边界被二者包在里面,最后left和target对应的值的下标全部在一个位置,此时刚好left>right了,跳出循环,将left赋给左边界即可。右边界同样处理即可。

即把握好左边界向左查找,右边界向右查找即可。

我们可以拿一个例子来理解:
若我们要找的左边界在刚开始就是nums数组中mid对应的下标,即刚开始我们其实就找到了左边界。但我们并不知道,因此又再向左查找,但此时由于right=mid-1,也就是接下来在left<right时我们怎么查找也找不到大于target的了,所以经过不断的循环后,最终left=right,但发现仍找不到,所以此时left=mid+1了,跳出了循环。而此时mid=right=left,所以此mid+1就是right+1,就是我们刚开始就找到的mid了。

并且我举的这个例子是必然情况,即你要找左边界,最后势必会来到我举的这个例子的情况,即mid对应的下标恰好就是左边界的情况。

为什么呢?
因为若不是那种情况的话,你通过二分查找不断缩小区间,最后也是到达这种情况。

所以可知代码如下:

int erfen(int*nums,int numsSize,int target,int index){
    
       
    int left=0;
    int right=numsSize-1;
    while(right>=left)//一定要为等于
    {
    
    
        int mid=(left+right)/2;//mid要放里面,因为每次循环mid都要改一次
        if(nums[mid]>target)
        {
    
    
            right=mid-1;
        }
        if(nums[mid]<target)//二分老规矩
        {
    
    
            left=mid+1;
        }
        if(nums[mid]==target)//当等于时不能直接赋予,因为可能真正的边界在左边或右边
        {
    
    
            if(index==0)//此为找左边界时,此点可能不是左边界,所以不妨向左再找找
            {
    
    
                right=mid-1;
            }
            else
            {
    
    
                left=mid+1;//同上,此为找右边界
            }
        }
    }
    if(index==0)//注意此时我们是默认一定能找到的情况,至于真的找不找的到由后面选择即可
    return left;
    else
    return right;
}
int* searchRange(int* nums, int numsSize, int target, int* returnSize){
    
    
    int *res=(int*)malloc(sizeof(int)*2);
    *returnSize = 2;
    res[0]=erfen(nums,numsSize,target,0);//左边界查找
    res[1]=erfen(nums,numsSize,target,1);//右边界查找
    if (!(res[0] <= res[1] && nums[res[0]] == target && nums[res[1]] == target)) 
    //正是因为原先二分查找是默认nums数组里一定有target的情况,所以我们要进行一次选择
    {
    
    
        res[0] = res[1] = -1;
    }
    return res;
}

猜你喜欢

转载自blog.csdn.net/xiangguang_fight/article/details/111871234