二分查找法相关例题讲解

相信大家在刷题过程中经常会遇见使用二分查找法处理问题的时候,二分查找分本身的思想并不难,相信大家也都能掌握,但是对于循环退出的条件,以及边界的处理上可能会存在些许的疑问,我通过leetcode上面的几道经典的二分查找法例题来帮助大家巩固这方面的知识。

举具体的例题之前,先和大家分享下二分查找的一些细节。

1、写之前先确定好左右边界的闭合情况,其实无论是左闭右开或者左闭右闭都是可以的,当为左闭右开时,数组的长度是[0,size)。当为左闭右闭时,数组的长度是[0,size-1)。其中的size是数组元素的个数,大家写之前一定要注意好这个问题。

2、循环退出的条件。当我们选择左闭右开时,千万不能写成while(left<=right),否则区间就不存在了。当写成while(left<right)这种情况时,退出循环的时候,恰好是left=right,可以返回任意一个端点。当我们使用左闭右闭的时候,可以写成while(left<=right),此时表示的是循环体内只有一个元素啥的时候,我们还需要继续查找,退出循环的时候,left和right是不相等,此时还要做比较来看具体返回什么值。

3、取中间值。一般代码为int mid=(left+right)/2  ,但这个代码严格意义来说是错的,当left和right都很大的时候,会产生溢出现象,我推荐这样写:int mid=left+(right-left)/2;

4、左右区间如何变化。当左闭时,left=mid+1,当右闭时,right=mid-1。当右开时,right=mid,真实取的值仍是mid-1。

(1)统计一个数字在排序数组中出现的次数。

看到这个题,大多数人可能一上来就觉得简单,不就是遍历整个数组嘛,然后统计数字出现的次数。但是,我想说的是,我们做题不是真正的为了去完成题目,而是更深的巩固我们的编程能力,本题提到排序数组,如果我们直接暴力循环来解题,这个条件岂不是用不上了?我们在以后的题目中,如若看到了题目中给定数组是排好序的,脑海里一定要想到二分查找法,二分查找的时间复杂度比暴力循环低很多,而且能很好的利用条件。 

普通的二分查找是查找某一个数,但是本题要查找多个数,但是不影响我们解题,我们可以通过示例发现,要找的数字一定是排列在一起的,也就是说我们可以通过二分查找法先找到第一个数字,然后在该数字的两边再次遍历,如果找到该数字则结果++,具体代码如下:

class Solution {
public:
    int search ( vector < int > & nums, int target ) {
        //我们采用左闭右闭的区间
        int left = 0, right = nums.size() - 1;
        int res = 0;
        //区间只剩一个数时仍需比较是否为target,所以为<=
        while ( left <= right )
        {
            int mid = left + ( right - left ) / 2;

            //找到了该数字,res++,定义两个指针分别为该数字的前后两个位置,再依次向前和
            // 向后遍历继续寻找等于target的数字
            if( nums[mid] == target )
            {
                ++res;
                int front = mid - 1;
                int last = mid + 1;
                //向前遍历
                while ( front >= left && nums[front] == target )
                {
                    ++res;
                    --front;
                }
                //向后遍历
                while ( last <= right && nums[last] == target )
                {
                    ++res;
                    last++;
                }
                return res;
            }
            //mid的值小于目标值时,left右移
            else if ( nums[mid] < target )
            {
                left = mid + 1;
            }
            else
            {
                right = mid - 1;
            }

        }
        return res;
    }
};

(2)一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

此题同样可以使用暴力循环算法,依次比较坐标值和坐标对应的值是否相等即可,但是还是希望能够在解题的基础上降低时间复杂度,结合题意的递增排序的数组,那么我们可以想到二分查找法解决本题。 

如何确定丢失的值在哪里呢,我们可以先比较mid的值和坐标值是否一样,如果一样,则说明mid的左边是顺序排列的没有问题,那么问题就在mid的右边,然后在缩小二分查找的范围,再依次比较。反之,如果mid的值和坐标值不一样,则说明mid的左边就出现了问题,缩小范围继续比较。

我们具体的代码如下所示:

class Solution {
public:
    int missingNumber ( vector < int > & nums )
     {
         //确定二分查找的边界,区间为左闭右闭
         int j = nums.size () - 1;
         int i = 0;
        //当区间里的元素仅剩一个时,仍需继续比较
         while ( i <= j )
         {
             int mid = i + ( j - i ) / 2;
             //如果mid值等于坐标值时,说明mid左边没问题,缩小范围至[mid+1,j]
             if ( nums[mid] == mid )
             {
                i = mid + 1;
             }
            //不等的话,说明左边存在问题,缩小范围至[i,mid-1]
             else
             {
                 j = mid - 1;
             }
         }
         
         /*很多人会在最后返回的值上面犯难,我们仔细分析一下,循环中最后一次执行时是
           i=j=mid,如果这个值有问题的话那么j就会减1,i的值不变,我们直接返回i即可。
           如果这个值没问题的话,i的值就会+1,同时说明整个数组中的值都是和坐标值一一
           对应的,那么漏掉的数就是数组的长度,即返回i就行了。*/
         return i;
     }
};

(3)给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

我们根据示例可以得出,如果目标元素大于数组的最后一个元素,则返回数组最后一个元素下标加1。根据示例2,我们要返回大于等于2的下标位置,即返回1。综上所述,我们可以发现,当mid 的值要小于target的值是,mid及mid左边的所有元素都不是我们返回的结果,要在[mid+1,right]的区间继续查找。

 具体的代码如下:

class Solution {
public:
    int searchInsert ( vector < int > & nums, int target ) {
         int left = 0, right = nums.size()-1, mid;
         //当区间仅剩一个元素时,仍需与target比较
         while ( left <= right )
         {
             mid = left + ( right - left ) / 2;
             //如果找到了目标值,则直接返回坐标即可
             if ( nums[mid] == target )
             {
                 return mid;
             }
             //若mid里的值大于目标值,则缩小区间为[left,mid-1],因为是左闭右闭区间,所以
             // right=mid-1,mid-1的值也可以考虑到
             else if ( nums[mid] > target )
             {
                 right = mid - 1;
             }
             //若mid里的值小于目标值,则缩小区间为[mid+1,right]
             else
             {
                 left = mid + 1;
             }
         }
         
         /* 若循环里找到了这个值,则直接返回mid,若没有找到,则需要返回插入该值的位置。
            当while最后一次循环时,left和right和mid的值相等,如果mid里的值大于目标值时
            此时right左移一格,left值不变,即直接返回left的值为插入位置。如果mid里的值
            小于目标值时,left右移一格,即为插入位置,所以只需返回left即可。*/
         return left;
    }
};

(4)你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

 简化下题意,就是说有若干个产品,但是产品出现了问题,需要找出第一个出现问题的产品,题目中给定了isBadVersion这个函数,利用这个函数可以判断出产品是否出现问题。

题目不难,下面写两种不同的代码,请大家好好体会一下二分查找的细节。

// The API isBadVersion is defined for you.
// bool isBadVersion(int version);

class Solution {
public:
    int firstBadVersion ( int n ) 
{
        //二分查找的区间是[1,n],左闭右闭的区间
        int left = 1, right = n, mid;
        //当区间只剩一个元素时,仍做判断
        while ( left <= right )
        {
            mid = left + ( right - left ) / 2;
            //当该mid的零件是坏的时,将区间缩小至[1,mid-1]
            if( isBadVersion ( mid ) )
            {
                right = mid - 1;
            }
            //当mid的零件是好的时,说明出问题的在后面,将区间缩小为[mid+1,n]
            else
            {
                left = mid + 1;
            }
        }
       /*循环最后一次是left=right=mid,当这个零件仍是坏的时,right左移一格,right的零件
         是好的,即返回left。当这个零件是好的时,那么left右移一格,是第一个坏的零件,仍
         返回left*/
        return left;
    }
};
// The API isBadVersion is defined for you.
// bool isBadVersion(int version);

class Solution {
public:
    int firstBadVersion ( int n ) 
{
        //二分查找的区间是[1,n],左闭右闭的区间
        int left = 1, right = n, mid;
        //当left和right相等时退出循环
        while ( left < right )
        {
            mid = left + ( right - left ) / 2;
            //当该mid的零件是坏的时,将区间缩小至[1,mid]
            if( isBadVersion ( mid ) )
            {
                right = mid ;
            }
            //当mid的零件是好的时,说明出问题的在后面,将区间缩小为[mid+1,n]
            else
            {
                left = mid + 1;
            }
        }
       /*循环退出的条件是left和right相等,此时它俩同时指向第一个换的零件,直接返回
         一个即可*/
        return left;
    }
};

上面两段代码,细心的朋友会发现有很多地方都不同。第一段代码的循环条件是while(left<=right),循环里面的内容是,当mid里的零件为坏的时候,right=mid-1。而第二段代码的循环条件是while(left<right),循环里面的内容却是right=mid。

大家可以先思考,也可以评论区与我交流。希望这篇文章能给大家加深二分查找的循环条件、返回何值、区间如何改变等知识,创作不易,如果觉得作者的内容对你有帮助,不要吝啬你的点赞和关注,是我继续分享创作的动力!

  • 7
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值