再写错二分法的边界,你就来打我!

二分法

二分法是用于查找有序序列中某个元素的常用方法,时间复杂度为log(n)

注:本文提及的有序均默认为升序,降序的序列是一样的思路,不做赘述

写过二分法的人就知道,二分法好用但是也经常让人头疼,因其边界往往容易出错,而且在刷力扣看题解的时候,一会儿while(left < right)一会儿while(left <= right),头都搞晕了,因此特地来学习总结一番

最容易出错的地方:

  • 1.while循环中到底是 小于 还是 小于等于

  • 2.左右边界left和right,到底是+1 呢,还是要-1呢

这两个问题的本质是一样的,取决于你如何定义左右区间,可以分为两种:

左闭右开

定义左边界left=0,右边界right=arr.length,由于数组的索引范围为0到arr.length-1,right=arr.length这个索引是越界的,所以你相当于定义了一个左闭右开的区间,[left,right)

所以你想想,是while(left < right)还是while(left <= right),如果说是后者,也就是left和right相等循环也能执行,那么当left=right=arr.length时也能进入循环,那数组不就越界了?

因此,当你定义的区间为左闭右开时,while里面一定是小于

第一个问题解决了,第二个问题我们进入代码说明,talk is cheap, show me the code

现在要从一个有序数组arr[n]中查找某个元素,代码如下:

public int binarySearch(int[] arr, int target){
    int left = 0, right = arr.length;
    while(left < right){
      // 等同于(left+right)/2,但是当left和right较大时两者相加容易发生溢出
      int mid = left + (right-left) / 2;
      // 找到了target就直接返回mid
      if(arr[mid] == target)	return mid;
      // 如果当前值大于target,而数组是升序的,说明target在数组的左半边[left,mid-1],但是由于区间定义是左闭右开,因此数组左半边写成[left,mid),所以right=mid
      else if(arr[mid] > target)	right = mid;
      // 最后,如果当前值小于target,则说明target在数组的右半边[mid+1,right),因此left直接等于mid+1就好了
      else left = mid + 1;
    }
  // 找不到就返回-1
  	return -1;
}

左闭右闭

思路和上面差不多,只不过是定义的区间变了,因此我在这里简要说明一下

定义左边界left=0,右边界right=arr.length-1,这个时候左右边界均在arr索引范围内,因此是左闭右闭的,[left,right]

因此是while(left <= right),即便是到了right边界,也不会发生数组溢出

这里我不单独贴出左闭右闭的代码,而是将两个写法的代码进行对比
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fZCP7iue-1627817256574)(../狂神说Redis笔记/力扣/力扣专题笔记.assets/image-20210801180524309.png)]
到这里,基本上就可以搞懂边界问题了

强烈建议你使用左闭右闭这样的区间定义,也就是while(left <= right),不容易出错

模板总结

上面的只是对边界问题的一个解释,下面对二分法进行一个模板式的总结

二分法其实有两种情况:

  • 1.数组或序列中没有重复元素
  • 2.有重复元素

没有重复元素的就是上面的写法,这里着重讲一下最常用的有重复元素的序列

(其实,如果没有重复元素,我为什么不用java工具类Arrays.binarySearch()呢,我还自己写个锤子!所以我们只需要手写有重复元素的二分法)

那我习惯左闭右闭的区间定义,这里用左闭右闭

找有重复元素的target又可以分为两种,比如arr[1,2,2,2,3],我要找2,那你是想找最左边的2还是最右边的2呢?可以称之为下边界上边界

// 下边界,也就是数组中第一次出现target的位置
public int binarySearchLowwer(){
  	int left = 0;
  	int right = arr.legth;
  	int pos = -1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] >= target) {
            // 中间的值要是大于target肯定往左区间找,如果中间值等于target呢
          	// 因为我们是找下边界,所以也是往左区间搜索,因此这两种情况可以合并
          	// 中间值大于等于target都往左区间[left,mid-1]查找
          	pos = mid;	// pos会被不断的往左更新,直到为下边界
            right = mid - 1;
        }
        else {
            // 这个简单,中间的数小于target,往右子区间[mid+1,right]查找
            left = mid + 1;
        }
    }
    return pos;
}
// 上边界,也就是数组中最后一次出现target的位置
public int binarySearchUpper(){
  	int left = 0;
  	int right = arr.legth;
  	int pos = -1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] > target) {
            // 中间的值要是大于target肯定往左区间找,如果中间值等于target呢
          	// 因为我们是找上边界,所以肯定往又区间找,所以放到else情况里面
            right = mid - 1;
        }
        else {
            // 中间的数小于等于target,往右子区间[mid+1,right]查找
          	pos = mid;	// pos会被不断的往左更新,直到为下边界
            left = mid + 1;
        }
    }
    return pos;
}

可以观察到,上边界和下边界只有两处代码不同

  • 下边界arr[mid]大于等于target,上边界arr[mid]大于target
  • pos的位置不一样

二分法总结完毕,觉得有用请点个赞支持啦!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值