二分法细节与核心

本文详细探讨了二分法的核心思想,指出二分不仅仅用于找数字,而是通过排除状态来逼近目标。内容包括普通二分查找的注意事项,如防止溢出,以及边界二分查找,包括左边界和右边界查找的实现。文章强调了在不同场景下调整搜索空间的重要性,并提供了相关代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

核心:

  • 二分并不是仅仅用于找数字的,二分的二,其本质是两种不同的状态,通过每次排除一半来逼近两种状态的分界线,在找数字中,我们利用的两种状态就是数字左右两边大小不一样。

二分细节

注意事项:

  1. 所有情况均为if else语句,更加清晰

  2. 注意防止mid溢出

普通二分查找

在nums中搜索target,找到返回下标,否则返回-1.

int binarySearch(vector<int> nums, int target) {
    int l = 0;
    int r = nums.size() - 1; // @1
    while(l <= r) // @2
    {
        int m = (r - l) / 2 + l;
        if(nums[m] == target) return m; // 找到元素,返回下标
        else if(nums[m] < target) l = m + 1; // @3
        else if(nums[m] > target) r = m - 1; // @4
    }
    return -1;
}

首先要明确,二分到最后肯定是不断逼近一个数字的,类比数学上二分找函数零点的思想。

上述的四处@,是四个注意的点,下面分别说一下(为配合上面的代码,建议将代码截图后,一边看解释一边看代码):

  • @1: r是nums.size()还是nums.size()-1?,这个选择其实是自由的,因为我们二分的目标在于,用尽量少的次数去找到目标值,其本质还是基于遍历的思想(不知道这么说是否准确,我的理解是,二分每次可以舍弃一半,但是这一半的舍弃我们确定不会对结果产生影响,所以舍弃,但是不能舍弃掉未知的元素。举个例子,这个nums数组有n个元素,那就必须n个元素都考虑在内,不能从第2个元素开始遍历)。既然要遍历,那么我们这里的rl,也就是在确定遍历的范围。我们知道元素的下标是0nums.size()-1,因此这里选择nums.size()还是nums.size()-1无所谓,只要保证搜索区间范围包含所有元素即可。对于选择nums.size(),那我们的搜索区间就是[l,r),选择nums.size()-1,我们的搜索区间是[l,r]

  • @2: l <= r还是l < r,这里的范围是由上面的搜索区间决定的。

    • 如果选择nums.size(),搜索区间是[l,r),那么这里就应该是l < r,可以用反证法,如果是l <= r,那么如果一直向r逼近的话,r保持不变,l最终等于r,就会越界,所以是l < r

    • 选择nums.size()-1,我们的搜索区间是[l,r],这里应该是l <= r,这时不怕越界,如果是l < r的话,l最大也就是r - 1,那么会导致最后一个元素漏掉。

    • 对于最后返回-1,l <= r,终止条件是l 等于 r + 1,区间为空,可直接返回;但如果选择l < r,终止条件是l 等于 r,但可能会漏掉一个元素(之所以可能,是因为如果l等于r等于nums.size(),那么无所谓,但如果是在内部,那么这个地方就没有顾及到)

    • 如果数组是{1, 2, 3, 4, 5, 6, 7},目标是3,那么就会找不到(已运行验证(注意这里说的是 r=nums.size()并且l < r的循环条件)),所以为了防止漏掉,要检查这个地方,即

    • if(l < nums.size() && nums[l] == target) return l;
      else return -1;
      
  • @3: 3与4是一个问题,放在一块说。有时候是l = m,有时候是l = m + 1,这里还是由于搜索空间决定的,这里左右边界的变化,决定了下一次的搜索空间。对于这个题,我们要保证所有的都遍历过,而现在nums[m]已经遍历过,那么不再需要遍历,下一次的空间就应该是[l, m - 1]或者[m + 1, r]。这是由一开始的搜索空间决定的,如果一开始是[l, r),那么经过m后,要保持相同格式,就应该是[l, m)

    [m +1, r)

边界二分查找

上面的二分可以查到目标值,但是如果有多个目标值,我们想要第一个或者最后一个的边界值又该怎么办呢?

要在下一次的搜索空间上下功夫:

左边界二分

核心代码:

while(l < r) { // 此处同上,由一开始的区间决定,不再解释,不过边界的一般习惯是 <
    int m = (r - l) / 2 + l;
    if(nums[m] == target) {
        r = m; // @1
    } else if(nums[m] > target) {
        r = m; // @2,m已经验证过,但这里保持m,是为了保持[l, r)的区间格式
    } else if(nums[m] < target) {
        l = m + 1; // @3
    }
}
if(l < nums.size() && nums[l] == target) return l; // 原因上面已谈及
else return -1;
  • @1: 当我们找到一个符合条件的,由于需要找最左边的,不能直接返回,而是缩小上界。那如果只出现一次,将其放在r中,但最后返回l,有影响吗?经验证,没有影响,如果只出现一次,在第一次遇见放到r中,那么l会不断逼近直到与r相等(这也就是循环条件),所以最后返回l也是可以的。
右边界二分

核心代码:

while(l < r) { // 此处同上,由一开始的区间决定,不再解释,不过边界的一般习惯是<
    int m = (r - l) / 2 + l;
    if(nums[m] == target) {
        l = m + 1; // @1
    } else if(nums[m] > target) {
        r = m; // 同上
    } else if(nums[m] < target) {
        l = m + 1; // 同上
    }
}
if(l > 0 && nums[l - 1] == target) return l - 1; // @2
else return -1;

在找右侧边界时,即最后一个,那么如果当前的m大了,为保持左闭右开,r = m,如果m小了,l = m + 1

在@1中,可以看到是增大了下届(如果这里不加1,可能会出现死循环,因为m是向下取整,在只有两个数时,无法区分,比如3和4,中点是3,如果3不符合条件则会一直循环),这里的+1对于返回值@2产生了影响,容易看到如果只有一个数字符合,这时候l又加1,那么应该返回l - 1

总结

本文简要介绍了关于二分法在代码实现上的相关细节, 二分法思想比较简单,但是细节很多, 需要多注意。

参考链接

https://zhuanlan.zhihu.com/p/79553968

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值