关于二分查找你不知道的事!

  二分查找,是个非常牛但是又让人感觉很普通的算法。牛是因为它的时间复杂度只有O(logn),普通是因为人人都知道它并且烂熟于心,刻在DNA里了。

 但是二分算法有几个小细节,却不见得每个人都能通透了解。

二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 while(left < right) 还是 while(left <= right),到底是right = middle呢,还是要right = middle - 1呢?这就会导致有的时候程序运行起来存在一些风险,可别小看这些风险,如果程序跑起来有bug,那就是大问题。

原因:

大家写二分法经常写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。

写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。

下面我用这两种区间的定义分别讲解两种不同的二分写法。

接下来就让我们来看看关于二分查找的一些你值得关注的小细节吧!

首先,我们要确定要进行二分查找的数据范围。常见的数据范围有左闭右闭[a,b]区间以及左闭右开[a,b)区间,别小看这点,一旦确定下来,接下来的代码就要围绕这个区间去展开

以下我们都以缩小右边界为例子

左闭右闭即[left, right]区间

对于[a,b]区间,我们知道它是左闭右闭区间,也就是说,当a=b的时候,它也是成立的。我们举个例子,当a=b=1的时候,区间为[1,1],显然在数学上这是成立的。所以当在比较left++和right--的过程中,他们是可以相等的,所以这时候就应该选择while(left<=right)的写法。

接下来就是right的选择了。因为mid>val,此时的mid显然不需要在下一个区间进行比较了,我们知道mid是大于val的,而且区间是左闭右闭,此时选择right=mid就是不合理的,多余的操作。对于mid-1的大小我们并不明确,因此我们要选择right=mid-1,以进行下一个区间的判断。

tip:如果target大于第一个元素,小于最后一个元素,但是数组里不存在这个数字,或者target小于第一个元素,right最终就会移动到 left的左边,然后就跳出循环。

如果大于最后一个元素,left到right的右边

 {
        int left = 0;
        int right = numssize - 1; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) 
        { 
            // 当left==right,区间[left, right]依然有效,所以用 <=
            int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
            if (nums[middle] > target) 
            {
                right = middle - 1; // target 在左区间,所以[left, middle - 1]
            } 
            else if (nums[middle] < target) 
            {
                left = middle + 1; // target 在右区间,所以[middle + 1, right]
            } 
            else  return middle; // 数组中找到目标值,直接返回下标                
        }
        // 未找到目标值
        return -1;
}

左闭右开即[left, right)区间

对于[a,b)区间,还是以缩小右边界我们知道它是左闭右开区间,也就是说,当a=b的时候,它是不成立的。我们举个例子,当a=b=1的时候,区间为[1,1),显然不成立,怎么可能存在区间即包含1,又不包含1。所以当在比较left++和right--的过程中,他们是不可以相等的,所以这时候就应该选择while(left<right)的写法。

接下来就是right的选择了。依然有mid>val,这里的区间是左闭右开。假设这里选择了right=mid-1。对于mid-1的大小我们并不明确,区间右边又是开的,是不包含的,那么下一个区间它就不在判断范围内,大家想想这合理吗?显然这是不合理的!如果我们选择right=mid,mid的大小我们知道,右区间是开的,mid此时不在下一个区间判断范围内,大小未知的mid-1在,这就十分合理。所以这里我们要选择right=mid;

{
        int left = 0;
        int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
        while (left < right)
         { 
            // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
            int middle = left + ((right - left) >> 1);
            if (nums[middle] > target) 
            {
                right = middle; // target 在左区间,在[left, middle)中
            } 
            else if (nums[middle] < target) 
            {
                left = middle + 1; // target 在右区间,在[middle + 1, right)中
            } 
            else  return middle; // 数组中找到目标值,直接返回下标
        }
        // 未找到目标值
        return -1;
 }

总结:

  • 对于区间我们的开闭我们要从头到尾贯彻到底,从数学区间的角度去思考需不需要包含,问题也就迎刃而解,一定一定不要死记硬背!!!

我的表达可能没那么清晰。关于排版,表达以及其他问题大家可以多多提建议,我会吸取努力改正进步的!

同时更加希望大家可以真正弄懂二分查找,实现零错误使用!!那将是我的荣幸

  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值