二分查找(讲解足够详细)

通俗二分查找(查找一个数)

前提条件:所需查找的序列(基本是数组)必须有序,且目标数唯一。

基本框架

int binarySearch(int[] nums, int target) {
    int left = 0, right = nums.length - 1; //①

    while(left <= right) { //②
        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;
}

二分法的原理高中数学就已经接触过,比较容易理解,但是想要通过代码复现却并不简单,主要列出以下几个注意点:

  • ①:这里右边界right既可以取nums.length - 1,也可以取nums.length,只需相应的对循环条件②做适当调整即可。

  • ②:上述示例代码里循环条件既可以取<=,也可以取<,只需牢记这是你设定的终止条件,只要它可以保证程序的正确终止且元素没有漏查即可

    这里循环条件结合①的右边界初始条件极容易出错,在此做详细说明:

    1、<=的查找区间为全闭区间[left, right],跳出条件为left > right,这里均为整型,所以条件等同于left == right + 1,此时查找区间为[right + 1, right],显然区间为空,正确跳出循环;

    这里一定要理解全闭区间的意思:可以这样想,二分查找是不断折半原区间直至结束的过程,这一过程中的“折半”是通过不断比较nums[mid]target的大小并重新将mid赋予leftright来实现的,如果将mid视作指针,则可以将这一过程看做该指针在leftright这一区间内反复横跳(并不断更新自己),由于循环条件为<=,即mid有可能取到这一区间内的任意值(包括两端端点),故查找区间为全闭区间[left, right]

    2、<的查找区间为左闭右开区间[left, right),跳出条件为left == right,此时查找区间为[right,
    right],显然区间不为空,但循环已经跳出,即此时nums[right]仍未查找,所以循环结束之后要补充查找这个漏掉的元素,即return nums[left] == target ? left : -1;

    同理,这里的左闭右开区间可以这样理解:mid指针在leftright这一区间内反复横跳过程中,由于循环条件为<,即mid永远取不到这一区间内的右端点值(因为一旦left==rightleftright就是“横跳”的mid指针),就直接跳出循环),故查找区间为左闭右开区间[left, right)

  • ③:一般情况下这里写left + (right - left) / 2 就和(left + right) / 2 的结果相同,写后者通常也并不出错,但诸多大神及力扣官方解答里采用的都是前者的标准写法,这样可以避免数值太大相加导致溢出的风险

  • ④和⑤:这里的left = mid + 1right = mid - 1也可以写成right = mid或者 left = mid + 1,取决于查找区间。
    1、当查找区间为全闭区间[left, right]时,当发现nums[mid]target的大小并不相等时,则下一步应该查找的区间显然是[left, mid - 1][mid + 1, right](因为mid已经比较过了,需剔除)。

    2、同理,当查找区间为左闭右开区间[left, right)时,当发现nums[mid]target的大小并不相等时,则下一步应该查找的区间显然是[left, mid)[mid + 1, right)(因为mid已经比较过了,需剔除)。

寻找左(右)侧边界的二分查找

直接考察基本二分搜索的情况一般不是太多,很多时候考察的是寻找边界的变体形式

前提条件:所需查找的数组必须有序,但不必严格单调,若目标数有多个,则找的是这一连续相等字串最左侧的数,若目标数仅有一个,则这里的“左(右)侧边界”要根据题意判断。

寻找左侧边界

int leftBoundSearch(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0, right = nums.length - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            right = mid - 1; //※※※
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        }
    }
    //※※※
    return left;
}

理解了前面的解释,则寻找左侧边界的代码不难写出,下面强调一下需要注意的地方,即上述代码中标记了※※※的位置。

1、在基本查找中,当nums[mid] == target时,我们直接返回此下标,因为目的就是寻找那个相等的数(这个数是唯一的,找到就不用再继续了,直接跳出循环),而在左侧边界查找中,当nums[mid] == target时,由于这个target可能有多个,我们应该缩小区间继续寻找,这里默认有序是增序(降序换个方向即可),故显然右边界right应该往左侧收缩,同时,此处的查找区间是全闭区间[left, right],故right = mid - 1

2、在基本查找中,循环外的程序最后是return -1,这是因为这里的目的是找到唯一target,如果找到了,则在循环中就直接返回索引程序结束了,只有找不到的情况下,才会触发循环终止条件,结束循环,此时并没有找到target,因此return -1表示未找到;而在左侧边界查找中,循环体内部并没有程序返回语句,因为我们要不断缩小区间直至找到那个最左侧target,也就是说这里的循环查找要不断执行,直到区间全部查找完毕(亦即触发循环终止条件)才结束循环,最后,如果找到了,则return left

如果找不到,分为两种情况:
一种是target数值大小处于[left, right]之间,但是数组内并没有这个数,故要判断nums[left]target是否相等;
还有一种是target数值大于[left, right]里的所有数,此时left == right + 1(循环终止条件),由于target数值大于[left, right]里的所有数,因此right始终等于nums.length - 1,即循环结束时left = right + 1 = nums.length,此时会出现索引越界,需要判断是否越界,故在return left前应该补上

    if (left > nums.length - 1 || nums[left] != target) {
        return -1;
    }

这里并未考虑target数值小于[left, right]里的所有数是否会导致数组越界,同理,后面的寻找右侧边界也没有考虑target数值大于[left, right]里的所有数是否会导致数组越界,其实,这是不需要考虑的,相信大家看完全文会有所体会,这里不再赘述

这里还有一个点需要注意:对于成功找到左边界的情况,最后是return left而不是return right,按照前面的讲解,可能大家已经形成固定印象,认为当循环条件为left <= right时,循环终止触发条件为left == right + 1,这么写数学逻辑上当然没错,但是在此处还有另一层细节上的理解。

(讨论的大前提是可以找到左边界,即数组中存在一个或多个target)以寻找左边界为例,考虑两种情况:

  1. 数组里仅有一个target时,记其下标为i,当循环至第一次nums[mid] == target亦即nums[i] == target时,此时右边界左缩,right已经变成了i - 1,由于仅有一个target,同时数组单调,即i左侧所有元素均小于target,后续循环中mid横跳范围为[left, i - 1],因此不会再出现下一次nums[mid] == target,也不可能出现nums[mid] > target,易知从此次循环直到结束,右边界right不会再变,区间的折半left的不断右移来实现,直至left == right + 1结束循环,此时left = (i - 1) + 1 = i,最终return left即为要找的左边界下标,无误。
  2. 数组里有多个target时,由于实际数组的不同会有不同情况,一种会演变成上述情况;还有一种情况是,在right的右移和left的左移过程中,可能会出现rightleft同时指向target,有人可能会认为left继续右移直至left == right + 1结束循环,此时return left就不是正确结果了,其实不然,若记多个target的最左侧边界下标为i,在left第一次指向targeti后,left就不会再变了,之后区间的折半right的不断左移来实现,直至right指向i - 1,此时的循环终止条件应理解为right == left - 1,最终return left即为要找的左边界下标,依旧无误。

因此完整的寻找左侧边界的代码如下:

int leftBoundSearch(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0, right = nums.length - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            right = mid - 1; 
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        }
    }
    if (left > nums.length - 1 || nums[left] != target) {
        return -1;
    }
    return left;
}

寻找右侧边界

与上述同理,极易写出寻找右侧边界的二分查找代码如下:

int rightBoundSearch(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0, right = nums.length - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            left = mid + 1; //此处应将左侧边界left往右缩
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        }
    }
    //右边界的左移可能会导致数组越界
    if (right < 0 || nums[right] != target) {
        return -1;
    }
    return right;
}

至此,二分查找的所有细节讲解完毕,注意,如果初始右边界选取nums.length,则循环条件必须是<,以避免数组越界,相应的内部细节稍作修改即可,理解透彻原理后并不难。
建议初始右边界选取nums.length - 1,循环条件取<=,这样便于理解,也便于记忆,同时统一了三种不同形式的二分查找。

相信以上内容已经足够详细,希望能对大家透彻掌握二分法有所帮助,码字不易,请尊重原创

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值