【算法】二分查找(上)

目录

一、写好二分查找的四个步骤

二、在排序数组中查找元素的第一个和最后一个位置

三、搜索插入位置

四、x的平方根


通过上篇文章【手撕二分查找】,我们知道了二分查找的【四要素】初始值、循环条件、mid的计算方式、左右边界更新语句

循环条件左右边界更新语句,决定了二分查找循环终止时left和right的位置(相错/相等/相邻)

初始值mid的计算方式,要使得中间下标mid覆盖且仅覆盖【搜索空间】中所有可能的下标

一、写好二分查找的四个步骤

1.确定区间形式(【左闭右闭】、【左闭右开】、【左开右开】...)

2.维护区间形式:为了维护区间形式,要设计初始值循环条件左右边界

满足:

·初始值:左右边界初始值的区间覆盖整个数组

·循环条件:满足区间形式的特点而设。如:【左闭右闭】在两者相错时,终止循环。所以循环条件为:left<=right。【左闭右开】在两者相遇时,终止循环。所以循环条件为:left<right

·左右边界:在每次搜索完后,需要排除已经搜索过的区间。

3.选择mid的计算方式:mid计算方式的选择,是为了帮助循环缩小区间,避免死循环。

通常都是采取向下调整,但是有时候需要向上调整。

如:在【左开右闭】中(left=mid、right=mid-1),若采取向下调整mid会在中间两个元素中选择较小的那一个位置,当左右指针相邻时,mid始终选择较小的,最终由于left=mid,导致死循环

解决方法,就是保证在左右指针相邻时,让mid选择 能够帮助缩小区间的(移动右指针)--向上调整

总结

根据左右边界的更新语句,让mid的计算方式 在左右指针相邻时,选择能够帮助缩小区间的一个(即:指针不指向mid)。

向上调整:mid会选择中间两个元素中较大的(nums[mid]>target)---移动右指针【左开右闭】

向下调整:mid会选择中间两个元素中较小的(nums[mid]<target)---移动左指针【左闭右开】

注意:【左闭右闭】、【左开右开】都是向下调整

上述三个步骤以及满足了二分查找的【四要素】,但是我们还需要注意返回值

4.返回值:通过分析数组中元素的三种情况(都大于、存在、都小于target)以及其对于的返回值,判断应该返回什么(最好是画图)

如:【左闭右闭】中,找出大于等于target的数字下标

考虑nums中元素的三种情况:

1.nums中所有元素都小于target时,right不更新,最终left=nums.length,因此当这个关系成立时,返回-1

2.nums中存在元素大于等于target时,由【循环不变】的关系可知,最终应该返回left

3.nums中所有元素都大于target时,left不更新,left=0,最终right=-1,此时应当返回下标0,因此返回left

二、在排序数组中查找元素的第一个和最后一个位置

题目链接34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

题目描述

给你⼀个按照⾮递减顺序排列的整数数组 nums,和⼀个⽬标值 target。请你找出给定⽬标值在数组

中的开始位置和结束位置。

如果数组中不存在⽬标值 target,返回 [-1, -1]。

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

⽰例 1:

输⼊:nums = [5,7,7,8,8,10], target = 8

输出:[3,4]

⽰例 2:

输⼊:nums = [5,7,7,8,8,10], target = 6

输出:[-1,-1]

⽰例 3:

输⼊:nums = [], target = 0

输出:[-1,-1]

提⽰:

0 <= nums.length <= 10^5

-10^9 <= nums[i] <= 10^9

nums 是⼀个⾮递减数组

-10^9 <= target <= 10^9

题目分析

我们需要在非递减数组中,找到目标值的开始位置和结束位置。只有当目标值在数组中至少存在一个时,才会有开始位置或结束位置。其余情况返回[-1,-1]

我们这里使用两次二分查找分别找开始位置(Search1)和结束位置(Search2)

Search1:我们只需要在数组中找到第一个大于等于target的数字下标,便可以表示开始位置

Search2:可以在数组中找到第一个大于target的数字下标,然后令其减1,即可表示结束位置

但是在求开始位置时,有一种情况无法求出。即:当数组中没有target时,Search1返回的数字下标是第一个大于target的数字下标

而Search2求的也是第一个大于target的数字下标。因为Search2最终减1,导致两指针相错.所以当两指针相错时,表示没有target,返回[-1,-1]

解题思路

1.确定区间形式:我们这里选择【左闭右闭】

2.维护区间形式初始值循环条件左右边界

初始值:left=0,right=nums.length-1;

循环条件:left<=right

左右边界:left=mid+1,right=mid-1

3.选择mid的计算方式:向下调整:mid=left+(right-left)/2

4.返回值:return left==nums.length?-1:left(Search1)、return left(Search2)--解释如下

注意:在这四个步骤中,第4步尤其需要注意,因为前三步很容易得出,但返回值需要考虑数组中元素的三种情况。

对于Search1:寻找第一个大于等于target的数字下标

考虑nums中元素的三种情况:

当数组中所有元素都小于target时,right不更新,最终left=nums.length。但此时应该返回-1

当数组中存在元素大于等于target时,按【循环不变】的关系,应该返回left

当数组中所有元素都大于等于target时,left不更新(0),最终right= -1,此时应该返回left(0)

当数组中最后一个元素等于target时,left=nums.length-1,right=left-1,此时返回left

对于Search2:寻找的是第一个大于target的数字下标

考虑nums中元素的三种情况:

当数组中所有元素都小于等于target时,right不更新,最终left=nums.length。但此时应该返回-1

当数组中存在元素大于target时,按【循环不变】的关系,应该返回left

当数组中所有元素都大于target时,left不更新(0),最终right= -1,此时应该返回left(0)

需要注意的是:

当数组最后一个元素(等于target)是结束位置时,此时left=nums.length,在减1后就表示数组最后一个元素。这里不能因为left=nums.length,就返回-1。所以我们的返回语句使用的是return left。这里的最后一个元素包含了数组只有一个元素的情况。

所以我们需要处理:当数组中所以元素都小于target时,返回 -1的情况

解题代码

class Solution {
        public static int[] searchRange(int[] nums, int target) {
        if(nums.length==0)return new int[]{-1,-1};
        int first=search1(nums,target);//找到第一个大于等于target的数字下标
        int end=search2(nums,target);//找到第一个大于target的数字下标
        //处理search2中,数组最后一个元素小于target的情况
        if(end>=1&&nums[end-1]<target)return new int[]{-1,-1};
        if(first<=end-1) return new int[]{first,end-1};
        else return new int[]{-1,-1};
    }
    //找到第一个大于等于target的数字下标
    private static int search1(int[] nums, int target) {
        int left=0;
        int right=nums.length-1;
        while(left<=right){
            int mid=left+(right-left)/2;
            if(nums[mid]<target)left=mid+1;//注意这里是 <
            else right=mid-1;
        }
        return left==nums.length?-1:left;
    }
    //找到第一个大于target的数字下标
    private static int search2(int[] nums, int target) {
        int left=0;
        int right=nums.length-1;
        while(left<=right){
            int mid=left+(right-left)/2;
            if(nums[mid]<=target)left=mid+1;//注意这里是 <=
            else right=mid-1;
        }
        return left;
    }
}

我们这里分别使用两次二分查找,一方面是为了让大家可以熟悉二分查找大致流程,另一方面可以让大家理解其中的不同之处。

当然还有很多其他题解:如:我们在数组中找到任意一个target,然后再左右移动找到边界即可。

三、搜索插入位置

题目链接35. 搜索插入位置 - 力扣(LeetCode)

题目描述

给定⼀个排序数组和⼀个⽬标值,在数组中找到⽬标值,并返回其索引。如果⽬标值不存在于数组中,返回它将会被按顺序插⼊的位置。
请必须使⽤时间复杂度为 O(log n) 的算法。
⽰例 1:
输⼊: nums = [1,3,5,6], target = 5
输出: 2
⽰例 2:
输⼊: nums = [1,3,5,6], target = 2
输出: 1
⽰例 3:
输⼊: nums = [1,3,5,6], target = 7
输出: 4

题目分析:题目要求返回目标值在数组中的插入位置。其实就是查找第一个大于等于target的数字下标。又因为时间复杂度为O(log n) ,我们果断使用二分查找

在上一个题目,我们使用了【左闭右闭】的区间形式,在本题,我们使用【左闭右开】。只是为了让大家熟悉每种区间形式的写法

四步骤

1.确定区间形式:【左闭右开】

2.维护区间形式初始值循环条件左右边界

初始值:left=0,right=nums.length

循环条件:left<right

左右边界:left=mid+1,right=mid

3.选择mid的计算方式:向下调整:mid=left+(right-left)/2

4.返回值:return left(如下)

考虑nums中元素的三种情况:

1.nums中所有元素都小于target时,right不更新,最终left=right=nums.length,因此当这个关系成立时,返回left/right

2.nums中存在元素大于等于target时,由【循环不变】的关系可知,最终可以返回left/right(相遇)

3.nums中所有元素都大于等于target时,left不更新,left=0,最终right=left=0,此时应当返回下标0,因此返回right/left

解题代码

public static int searchInsert(int[] nums, int target) {
        //区间形式:【左闭右开】--据此设计初始值,循环条件,左右边界,mid计算方式
        int left=0;
        int right=nums.length;//初始值
        while(left<right){//循环条件
            int mid=left+(right-left)/2;//向下调整
            if(nums[mid]<target)left=mid+1;//边界设计
            else right=mid;
        }
        return left;
    }

四、x的平方根

题目链接69. x 的平方根 - 力扣(LeetCode)

题目描述

给你⼀个⾮负整数 x ,计算并返回 x 的 算术平⽅根 。
由于返回类型是整数,结果只保留 整数部分 ,⼩数部分将被 舍去 。
注意:不允许使⽤任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5
⽰例 1:
输⼊: x = 4
输出: 2
⽰例 2:
输⼊: x = 8
输出: 2
解释:
8 的算术平⽅根是 2.82842... , 由于返回类型是整数,⼩数部分将被舍去。

提示:

  • 0 <= x <= 2^31 - 1 (2,147,483,647)

题目分析:只需要在一段区间(可以是[0,根号x])内找到一个数t,满足 t*t<=x,(t+1)*(t+1)>x

其实也就是在区间内,找到第一个数 t,使其的平方 小于等于x即可

这个区间内的数是有序的,直接使用二分查找

解法1(左闭右开)

1.确定区间形式:【左闭右开】

2.维护区间形式:初始值循环条件左右边界

初始值:left=0,right=x/2+1;//由于根号x<=x/2(当x不等于0或1时),并且因为右侧边界为开,所以我们这里右侧边界设为x/2+1

这里将右侧边界设为x/2+1,还有一个好处,不需要额外处理x=0或x=1的情况

注意:我们只需要保证初始值的范围覆盖了整个【搜索空间】即可。在这里满足right*right>=x

循环条件:left<right

左右边界:left=mid+1,right=mid

3.选择mid的计算方式:向下调整:mid=left+(right-left)/2

4.返回值:return left;//但本题需要额外处理

额外处理:

我们知道当left=right时,循环终止。

在本题中,需要注意,当left向右侧移动时(mid*mid<x-->left=mid+1),若此时left与right相遇,循环终止,mid无法更新。我们判断了(mid*mid<x),但是返回的left=mid+1,我们还需要额外判断新的mid(在循环外侧,再计算一次mid)处的平方是否大于x,如果大于x,返回的应该是left-1

注意:由于本题0 <= x <= 2^31 - 1 (2,147,483,647),mid*mid可能会超出int类型,需要转换为long类型

解题代码:

class Solution {
    public static int mySqrt(int x) {
        //左闭右开
        //if(x==0||x==1)return x;
        int left=0;
        int right=x/2+1;//这里处理了x=0或x=1的情况
        int mid=0;
        while(left<right){
            mid=left+(right-left)/2;//这里不再是下标,而是中间值
            if((long)mid*mid<x)left=mid+1;
            else right=mid;
        }
        mid=left+(right-left)/2;
        if((long)mid*mid>x)return left-1;
        return left;
    }
}

解法2(左开右开)

使用【左开右开】的区间形式解决本题,有一个好处:因为左右边界都移动到mid处,当mid*mid<=x时,left=mid;否则right=mid。当left+1=right时,循环终止,left*left<=x,right*right>x

这时,可以直接返回left,不需要额外判断 新的mid处的平方是否大于x。

四步骤

1.确定区间形式:【左开右开】

2.维护区间形式初始值循环条件左右边界

初始值:left=0,right=x/2+2//注意我们这里设置right=x/2+2,因为当x等于0或者1时,我们让(left,right)=(0,2),这样仍然能进入循环(left+1<right),得出最终结果。而这里left设置为-1或0都可以。只需要保证当特殊情况x=0/x=1时,仍然能进入循环即可

循环条件:left+1<right

左右边界:left=mid,right=mid

3.选择mid的计算方式:向下调整:mid=left+(right-left)/2

4.返回值:return left

解题代码

class Solution {
    public static int mySqrt(int x) {
        //左开右开
        int left=0;//-1也可以
        int right=x/2+2;//重点
        while(left+1<right){
            int mid=left+(right-left)/2;//这里不再是下标,而是中间值
            if((long)mid*mid<=x)left=mid;
            else right=mid;
        }
        return left;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值