二分查找详解(区间,模板)

文章讲述了二分查找算法在有序数组中的应用,介绍了三种区间处理方法:左闭右闭、左闭右开和左开右闭,并提供了代码示例。作者还扩展了二分查找的模板,用于在数组中寻找特定元素的插入位置、边界位置以及在旋转排序数组中的应用。
摘要由CSDN通过智能技术生成

目录

1.从算法角度看二分

(1)问题

(2)二段性

2.三种区间

(1)[left,right]左闭右闭

(2)[left,right)左闭右开

(3)(left,right]左开右闭

3.二分模板


1.从算法角度看二分

(1)问题

考虑如下问题:

给你一个有序的含有1000个元素的数组arr[1000],求待查找元素target的下标idx。

 直观地,用暴力法解决上述问题,用一个for循环遍历整个数组,用if判断来遍历的元素是否等于target。考虑最坏情况,我们需要遍历整个数组,时间复杂度是O(n)的。

(2)二段性

如何进行优化呢?

在上述暴力做法中,我们没有利用[数组有序]这一条件,如果我们取数组中某个元素x,通过比较这个元素x和target的大小,如果x>target,我们可以立刻排除x及右边的所有元素,这些元素都是大于target的,同理x<target,我们可以排除x及左边的元素,它们都小于target。即我们可以发现数组中有二段性,一部分是大于target的一部分是小于target的,因此我们要找到满足性质转换的那个点,也就是target。

当然「二分」不是单纯指从有序数组中快速找某个数,这只是「二分」的一个应用。

「二分」的本质是两段性。只要一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。

2.三种区间

二分查找的难点在于对于区间细节的处理,常见的写法有左闭右闭,左闭右开,左开右闭三种。

对查找有序数组中的某个元素的问题,我们在利用二分法进行判断的时候,需要检索区间,而初始待检索的区间有几种写法:[left,right],[left,right),(left,right],以上区间写法取决于我们初始化left和right时是如何赋值的。

(1)[left,right]左闭右闭

当我们进行如下初始化:

//len为数组长度
int left=0,right=len-1;

表示我们所用的是左闭右闭的写法, 初始化查找的区间是整个数组,并且左右端点都会取到,所以后面的代码应注意:

  1. 循环条件要使用while(left <= right),试想如果数组只有一个元素时,初始时left==right,为了保证能够进入循环,需要添加=
  2. if(arr[mid] > target) , right 要赋值为 mid - 1, 因为已经判断当前的 arr[middle] 一定不是 target ,需要把这个 mid 位置上面的数字丢弃,那么接下来需要查找范围就是[left, middle - 1]
  3. 求mid时,如果直接利用(right+left)/2来计算,如果left和rigth较大时,求和可能会溢出,所以改用left+(right-left)/2,后半部分做减法,防止溢出。
int search(int[] arr,int target){
    int len=arr.length;
    int left=0,right=len-1;
    while(left<=right){
        int mid=left+(right-left)/2;
        if(arr[mid]==target) return mid;//找到了直接返回
        else if(arr[mid]>target) right=mid-1;//说明target在mid的左边
        else left=mid+1;//说明target在mid的右边
    }
    return -1;//未找到
}


(2)[left,right)左闭右开

当我们进行如下初始化:

//len为数组长度
int left=0,right=len;

表示我们所用的是左闭右开的写法,对比上述左闭右闭的写法,初始时由于right无法取到,我们查找的区间是[left,right),根据区间的定义,后续条件控制应该如下:

  1. 循环条件使用while (left < right)
  2. if (arr[middle] > target) right = middle;因为当前的 arr[middle] 是大于 target 的,不符合条件,不能取到 middle,并且区间的定义是 [left, right),刚好区间上的定义就取不到 right, 所以 right 赋值为 middle
int search(int[] arr,int target){
    int len=arr.length;
    int left=0,right=len;
    while(left<right){
        int mid=left+(right-left)/2;
        if(arr[mid]==target) return mid;//找到了直接返回
        else if(arr[mid]>target) right=mid;//说明target在mid的左边
        else left=mid+1;//说明target在mid的右边
    }
    return -1;//未找到
}


(3)(left,right]左开右闭

与上述左闭右开类似,这种写法用的比较少,写法如下:

int search(int[] arr,int target){
    int len=arr.length;
    int left=-1,right=len;
    while(left<right){
        int mid=left+(right-left+1)/2;//防止溢出
        if(arr[mid]==target) return mid;//找到了直接返回
        else if(arr[mid]>target) right=mid-1;//说明target在mid的左边
        else left=mid;//说明target在mid的右边
    }
    return -1;//未找到
}

3.二分模板

在数组arr[]中寻找第一个大于等于target的位置。

左闭右闭写法

int search(int[] arr,int target){
    int len=arr.length;
    int left=0,right=len-1;
    while(left<=right){
        int mid=left+(right-left)/2;
         if(arr[mid]>=target) 
            //每次更新之后,arr[right+1]>=target一定成立
            //也就是说最后的right+1始终指向大于等于target的位置
            right=mid-1;
        //同理。left-1始终指向小于target的位置
         else left=mid+1;
    }
    //最后循环结束后,right+1指向第一个大于等于target的位置,而循环结束时left恰好在right右边
    //即left=right+1,返回left即可
    return left;
}

左闭右开写法

int search(int[] arr,int target){
    int len=arr.length;
    int left=0,right=len;
    while(left<right){
        int mid=left+(right-left)/2;
         if(arr[mid]>=target) 
            //每次更新之后,arr[right+1]>=target一定成立
            //也就是说最后的right始终指向大于等于target的位置
            right=mid;
        //同理。left-1始终指向小于target的位置
         else left=mid+1;
    }
    //最后循环结束后,right指向第一个大于等于target的位置,而循环结束时left恰好等于right
    //即left=right,返回left和right都可,这里返回left
    return left;
}

拓展:

1.在数组arr[]中寻找第一个大于target的元素位置:大于target的第一个位置即第一个大于等于target+1元素的位置,调用search(arr,target+1)

2..在数组arr[]中寻找第一个小于target的元素位置:即第一个大于等于target元素的前一个位置,调用search(arr,target)-1

3.在数组arr[]中寻找第一个小于等于target的元素位置:即第一个大于target元素的前一个位置,调用search(arr,target+1)-1


以对于有序数组而言,下面列举一些可以套用模板的情况:

1.leetcode 35.搜索插入位置

在数组中找到目标值target,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 

解决方案:调用search(arr,target)即可

2. leetcode 34.在排序数组中查找元素的第一个和最后一个位置

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

查找开始位置,start=search(arr,target),这里得到了第一个大于等于target的位置。我们必须确保start不越界且start对应的元素正好是target,即nums[start]==target && start<=len-1;查找结束位置,如果start存在,那么end一定存在,找第一个大于target元素的前一个位置即可,end=search(arr,target+1)-1。

类似的有:统计目标成绩的出现次数

3. leetcode 33. 搜索旋转排序数组 

所给数组具有二段性,利用左闭右闭写法,判断目标值是否处在某个单调递增区间,不断二分范围。

class Solution {
    public int search(int[] nums, int target) {
        int len = nums.length;
        if(len == 0) return -1;
        int left = 0, right = len - 1;
        while(left <= right){
            int mid = left + (right - left) / 2;
            if(nums[mid] == target) return mid;
            // 右边有序
            if(nums[mid] < nums[right]){
                // 目标值在右边
                if(target > nums[mid] && target <= nums[right]){
                   left = mid + 1;
                // 目标值在左边
                }else{
                   right = mid - 1;
                }
            // 左边有序
            }else{
               // 目标值在左边
                if(target >= nums[left] && target < nums[mid]){
                   right = mid - 1;
                // 目标值在右边
                }else{
                   left = mid + 1;
                }
            }
        }
        return -1;
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值