【二分查找递进式刷题】Leetcode刷题之二分查找

有人相爱,有人夜里看海,有人leetcode可以写一个礼拜。
呜呜呜是本人了

在这里插入图片描述

回顾一个十分简单的场景,猜数字游戏。我与友人A进行猜数字游戏,我心中想一个[0,100]的数字(假设我想一个10),让A来猜,友人A先猜20,我说大了,那他之后会猜多少的值,肯定是[0,19]的数值,然后他说5,我说大了,接着会他会猜多少,是[6,19]的数,我和A默契还不错,她直接一个10,猜对了~

这个过程就是一个完美体现了二分思想,注意是思想。什么思想?在每一次查找之后都会将下一次查找范围缩小到一个区域。 这句话后面依旧会提到,要十分牢记。

二分法与场景不同的点在于,二分。二分法每一次查找都是对半分的,这是与上面场景不同的点,毕竟猜数字嘛,大家随意猜的。但是使用对半分可以减少盲目的查找,这也与随意不同的地方了。

了解了二分思想之后,直接上题目。

第一题:初步认识二分查找

leetcode704.二分查找
在这里插入图片描述

思路分析

我们着重于两个点:目标值,以及每一次查找的范围。题目就迎刃而解了
初次查找的时候,查找范围为数组首尾两个位置。我们定义两个指针在这两个地方,取其中间值,然后将这个中间值与target比较。进行的每一次比较我们都要缩小查找范围

  • 如果中间值middle>target,那么查找的返回将缩小到middle的左边
  • 如果中间值middle<target,那么查找的返回将缩小到middle的右边
  • 如果中间值middle=target,那么就说明查找到了,返回即可

重复上面的操作,直到找到或者left到了right的右边(这就说明这个数组中没有相关值,返回-1)
注意点:缩小范围的时候我们说如果中间值middle>target,那么查找的返回将缩小到middle的左边,也就是指下一刻,right = middle - 1,为什么是middle-1而不是middle?此时我们判断的条件是middle>target,也就是说这个middle值比如不是目标值,所以可以放心跳过它,直接到[left,middle-1]的范围查找,排除了不必要的查找过程。
图解如下:
初始的时候:
在这里插入图片描述
第二次查找(以9为例子):
middle等于9,找到返回。
在这里插入图片描述

完整代码

int search(int* nums, int numsSize, int target){
    int middle = 0;
    int left = 0;
    int right = numsSize - 1;
    while(left <= right)
    {
        middle = left + (right - left) / 2;//防止溢出,leetcode这题可以写(right+left)/2的
        if(target > nums[middle])
        {
            left = middle + 1;
        }
        else if(target < nums[middle])
        {
            right = middle - 1;
        }
        else 
        {
            return middle;
        }
    }
    return -1;
}

第二题:理解边界值left

leetcode35.搜索插入位置
在这里插入图片描述

思路分析

看见logn和排序数组的字眼了嘛,直接就激起了我对二分的敏感了哈哈哈哈哈
这道与704类似,但是稍微难一点的地方在于寻找没有找到的时候,插入位置的确认,需要对left和right的指向有一定理解就很好做了!
画图理解一下:
初始第一次查找:
在这里插入图片描述
第二次查找:
在这里插入图片描述
第三次查找:
此时left>right,我们的查找已经结束了。返回值就是left指向的位置。

注意前面说到,每一次查找都在缩小范围,这个范围指的是目标值的范围,那么第二幅图中,当left=right=middle的时候,此时的范围就是[left,right],也就是说right右边都不是查找范围了,而此时是由于middle<target,所以才导致了left右移的。middle右边的值又都是比target大才导致right左移的,所以,middle的这个位置,是插入的前一个位置,那么left=middle+1就是答案了。

着重理解每一次left和right表示的意义。

在这里插入图片描述

完整代码

int searchInsert(int* nums, int numsSize, int target){
    int left = 0;
    int right = numsSize - 1;
    int tmp = 0;
    while(left<=right){
        tmp = (left+right)/2;//也可以使用防止溢出的写法
        if(nums[tmp] > target){
            right = tmp - 1;
        }else if(nums[tmp] < target){
            left = tmp +1;
        }else{
            return tmp;
        }
    }
    return left;
}

第三题:理解边界值right

leetcode69.求Sqrt(X)
在这里插入图片描述

思路分析

我们使用第二题的思路来想,第二题中,我们的目标值如果在这个数组中就返回,如果不在,就返回数组中最后那个小于它的数后面的第一个位置。
而这题的目标值呢?这组整数中,如果存在恰好是平方根的整数就返回,如果不存在,就返回小于平方根的最近的第一个整数,其实就是返回上一个题中的right!

int mySqrt(int x){
    int left = 0;
    int right = x;
    while(left<=right){
        long long int middle = (left + right) / 2;//防止溢出的代码在这里就起作用了,这里使用防止溢出可以通过
        long long int m2 = middle * middle;
        if(m2 > x){
            right = middle - 1;
        }else if(m2 < x){
            left = middle + 1; 
        }else{
            return middle;
        } 
    }
    return right;
}

第四天:主动寻找边界

leetcode34.在排序数组中查找元素的第一个和最后一个位置
在这里插入图片描述
在看完前三题之后,如果说对于left和right的每一次取值的意义,以及对于二分思想:每一次都是在缩小范围有了理解之后,来道中等题吧!

思路分析

在前面我们一直没有提到的一个知识点,在这道题中展现出来了,但是呢这个点依旧是在说一个思想,每次查找必然是在缩小范围的。不过在写题的时候,一定会有相关疑问的-----对于偶数个数的数组,我们取middle的时候,可以取到两个值,但是前面我们都只取了前一个值,没有取后一个值,是不是所有题都可以只取前面的值呢?我们先带着疑问我们来写这题。
这题我们分隔成两个二分法来求解,第一个二分法来求左边界值,第二个二分法求右边界值。

求左边界:
在以往的写法中,当我们寻找到了middle = target的情况下,我们是直接返回的,但是现在不同了,因为target是多个的。我们要找的是个范围。当第一次找到middle的时候可以确定剩下存在的middle一定是在middle的左边或者就是middle的,所以将范围缩小到[left,middle],到最后的left如果与target相等,那么就肯定是左边界了

 int left(int *nums, int numsSize, int target){
    int left = 0;
    int right = numsSize - 1;
    while(left < right){
        int middle = (left + right) / 2;
        if(target < nums[middle]){
            right = middle - 1;
        }else if(target > nums[middle]){
            left = middle + 1;
        }else{
            right = middle;
        }
    }
    int leftside = left;
    if(nums[leftside] == target) return leftside;
    else return -1;
 }

求右边界:
如果是如下写法,那么说明踩坑了,这里就与上面我们说到的疑问相关了。

int right(int *nums, int numsSize, int target){
    int left = 0;
    int right = numsSize - 1;
    while(left < right){
        int middle = (left + right) / 2;
        if(target < nums[middle]){
            right = middle - 1;
        }else if(target > nums[middle]){
            left = middle + 1;
        }else{
            left = middle;
        }
    }
    int rightside = right;
    if(nums[rightside] == target) return rightside;
    else return -1;
 }

假设我们的数组为[1,1],寻找右边界的时候,middle会一直等于left,陷入循环,寻找右边界,那么遇到相同值之后肯定是将值向右边靠近的,正确写法如下:

int right(int *nums, int numsSize, int target){
    int left = 0;
    int right = numsSize - 1;
    while(left < right){
        int middle = (left + right + 1) / 2;//这里要+1,表示值向右靠近
        if(target < nums[middle]){
            right = middle - 1;
        }else if(target > nums[middle]){
            left = middle + 1;
        }else{
            left = middle;
        }
    }
    int rightside = right;
    if(nums[rightside] == target) return rightside;
    else return -1;
 }

完整代码

 int left(int *nums, int numsSize, int target){
    int left = 0;
    int right = numsSize - 1;
    while(left < right){
        int middle = (left + right) / 2;
        if(target < nums[middle]){
            right = middle - 1;
        }else if(target > nums[middle]){
            left = middle + 1;
        }else{
            right = middle;
        }
    }
    int leftside = left;
    if(nums[leftside] == target) return leftside;
    else return -1;
 }
int right(int *nums, int numsSize, int target){
    int left = 0;
    int right = numsSize - 1;
    while(left < right){
        int middle = (left + right + 1) / 2;
        if(target < nums[middle]){
            right = middle - 1;
        }else if(target > nums[middle]){
            left = middle + 1;
        }else{
            left = middle;
        }
    }
    int rightside = right;
    if(nums[rightside] == target) return rightside;
    else return -1;

 }
int* searchRange(int* nums, int numsSize, int target, int* returnSize){
    if(numsSize == 0) {
        *returnSize = 2;
        int *arr = (int*)malloc(sizeof(int)*2);
        arr[0] = -1;
        arr[1] = -1;
        return arr;
    }
    int l = left(nums, numsSize, target);
    int r = right(nums, numsSize, target);
    *returnSize = 2;
    int *arr = (int*)malloc(sizeof(int)*2);
    
    arr[0] = l;
    arr[1] = r;
    return arr;
}

最后

总的来说,二分法不是一个很难的知识点,理解好每一次查找的范围,我觉得这是十分有益于做题的。
代码不难,希望对大家有帮助,如果出现错误,及时指正我哟~祝大家学习愉快!!!

在这里插入图片描述

评论 55
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值