【Java算法专场】二分查找(上)

目录

 前言

什么是二分查找?

二段性

​​​​​​​​​​​​​​​​​​​​​二分查找

 算法分析

算法步骤

 算法代码

算法示例

模板

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

算法分析

算法步骤

算法代码

算法示例

搜索插入位置

算法分析

算法步骤

算法代码

算法示例

 x 的平方根

算法分析

算法步骤

算法代码

算法示例 ​​​​​​​


 前言

我们在做算法题时,遇到在数组中找特定元素时,通常会使用暴力解法来遍历数组中的元素,时间复杂度通常为O(n),但有时有些题目要求我们的时间复杂度要达到O(logN),那么我们就得使用二分查找来解决问题。

什么是二分查找?

二分查找是一种在有序数组中查找特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且同样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。

但二分查找只能在有序的数组中使用吗?

其实只要有二段性,就能够使用二分查找。

二段性

二段性指的是在查找过程中,数据集可以被分成两个部分,其中一部分满足某个条件,而另一部分不满足这个条件。换句话说,就是数据集可以被划分为两个子集,其中一个子集的所有元素都具有某种性质,而另一个子集的所有元素都不具有这种性质。

接下来就通过题目来讲解。

​​​​​​​​​​​​​​​​​​​​​二分查找

 算法分析

本道题是要在一个升序的数组中找到一个目标值,我们首先能想到的就是使用暴力解法,遍历数组,判断目标值是否在数组中,这样的解法时间复杂度为O(n),但我们可以进行优化,使用二分查找,能让时间复杂度达到O(logn)。

算法步骤

  1. 初始化:定义两个指针left和right,left初始化为0,right初始化为nums.length-1,即数组的末尾元素。
  2. 与中间值比较:定义mid,mid在循环中每次都是left+(right-left)/2(为了防溢出),判断nums[mid]和target的大小。循环条件为:left<=right

         1.若nums[mid]小于target,则让left=mid+1,

         2.若nums[mid]大于target,则让right=mid-1,

         3.当nums[mid]==target,说明此时已经找到了目标值在数组中的位置,返回mid即可。

     3.循环结束:当left和right错开时,说明此时已经查找完数组,没有找到目标值,返回-1即可。

 算法代码

/**
     * 在排序数组中查找目标值的索引。
     * 该方法使用二分查找算法,在一个已排序的数组中查找目标值的索引。如果目标值存在于数组中,则返回其索引;
     * 如果不存在,则返回-1。二分查找算法通过不断缩小搜索范围来提高查找效率。
     *
     * @param nums 一个已排序的整数数组。
     * @param target 要查找的目标整数。
     * @return 目标值在数组中的索引,如果不存在则返回-1。
     */
    public int search(int[] nums, int target) {
        /* 定义搜索范围的左右边界 */
        int left = 0, right = nums.length - 1;
        
        /* 当左边界不大于右边界时,进行循环搜索 */
        while (left <= right) {
            /* 计算中间位置,避免整数溢出 */
            int mid = left + (right - left) / 2;
            
            /* 如果中间位置的值小于目标值,则目标值在右半部分,更新左边界 */
            if (nums[mid] < target) {
                left = mid + 1;
            }
            /* 如果中间位置的值大于目标值,则目标值在左半部分,更新右边界 */
            else if (nums[mid] > target) {
                right = mid - 1;
            }
            /* 如果中间位置的值等于目标值,则找到目标值,返回其索引 */
            else {
                return mid;
            }
        }
        
        /* 如果循环结束还未找到目标值,返回-1表示未找到 */
        return -1;
    }

时间复杂度为O(logn),n为数组长度,整个过程中,每次都是排除一半的长度.

空间复杂度为O(1),整个过程中只用到了几个变量。

算法示例

以nums= [-1,0,3,5,9,12], target = 9为例

第一步:初始化双指针,让left指向数组起始位置,right指向数组末尾。

第二步:与中间值进行比较,让mid=left+(right-left)/2; 

 我们可以看图中,此时mid下表为2,nums[mid]=3<9,让left=mid+1;

此时left=3,mid=3+(5-3)/2=4,此时恰好nums[mid]=target=9,返回mid即可。

模板

上述中这种解法就是朴素的二分查找,我们可以总结出一个模板

 int left=0,right=数组名.length-1;
        while(left<=right){
            int mid=left+(right-left)/2;
            if(条件) {
                //处理
                left=mid+1;
            }else if(条件){
                //处理
                right=mid-1;
            }else{
                //处理
                return mid;
            }
        }

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

 

算法分析

题意说明了要在一个排序数组中找目标值的起始位置和末尾位置,对于这道题,如果我们采用暴力枚举的话,时间复杂度能达到O(n),但题目要求我们要实现复杂度为O(logn)的算法,对于这种在有序数组中找点的题目,我们可以用二分查找。

在讲算法步骤之前,我们先来了解一下找中点的问题。

我们知道使用mid=left+(right-left)/2,能够找到中点位置,但在偶数个数的情况下,若使用这条式子,则是向下取整,找的是靠左的中点。

但如果我们想要找的是靠右的中点,那么我们可以在(right-left)之中+1,表示向上取整。

即mid=left+(right-left+1)/2

总结:

在偶数个的情况下:

  1.  找左中点:mid=left+(right-left)/2
  2. 找右中点:mid=left+(right-left+1)/2

但在奇数个的情况下,使用哪个都行。

算法步骤

  1. 初始化: 设置left和right,left初始化为0,right初始化为数组的末尾位置。并创建一个ans数组(长度为2)初始化为{-1,-1}。
  2. 排除数组为空的情况:判断数组长度是否0,若为0,则返回ans数组。
  3. 找左端点:为了找到目标值最左侧的位置,当我们nums[mid]==target时,此时可能不是目标值的最左侧位置。因此,在nums[mid]>=target的时候,让right=mid,为什么不能让right=mid-1,这是为了防止跳过目标值,从而找不到最左侧的位置。当nums[mid]<target时,说明左侧期间[left,mid]中都小于目标值,让left=mid+1循环条件为left<right.(此处不能取等,在left和right相遇的时候,说明此时已经找打了目标位置,或者是已经走完数组,没有找到目标值,所以当循环结束之后,我们需要判断right位置的元素是否等于target,若等于,则让ans[0]=right,反之,则返回ans)。
  4. 找右端点:在查找右端点时,我们需要注意:

找中点时需要+1:mid=left+(right-left+1)/2.为了找到右侧的目标位置。

当nums[mid]<=target的时候,left=mid,为什么不能让left=mid+1,这是防止跳过目标值。当nums[mid]>target时,让right=mid-1.

算法代码

    /**
     * 在排序数组中查找给定目标值的起始和结束位置。
     *
     * @param nums 一个按升序排列的整数数组。
     * @param target 需要查找的目标值。
     * @return 一个包含起始和结束位置的整数数组。如果目标值不存在,则起始和结束位置都设置为-1。
     */
    public int[] searchRange(int[] nums, int target) {
        // 初始化结果数组,用于存储目标值的起始和结束位置,初始值设为-1。
        int[] ans=new int[2];
        ans[0]=ans[1]=-1;

        // 初始化左右指针。
        int left=0,right=nums.length-1;

        // 如果数组为空,则直接返回结果数组。
        if(nums.length==0) return ans;

        // 使用二分查找法寻找目标值的起始位置。
        while(left<right){
            int mid=left+(right-left)/2;
            if(nums[mid]>=target) right=mid;
            else left=mid+1;
        }

        // 如果右指针指向的值不等于目标值,则目标值不存在,直接返回结果数组。
        if(nums[right]!=target) return ans;
        else ans[0]=right; // 否则,更新起始位置。

        // 重新初始化左右指针,寻找目标值的结束位置。
        // 找右端点
        left=0;right=nums.length-1;

        // 使用二分查找法寻找目标值的结束位置。
        while(left<right){
            int mid=left+(right-left+1)/2;
            if(nums[mid]<=target)  left=mid;
            else right=mid-1;
        }

        ans[1]=left; // 更新结束位置。
        return ans; // 返回结果数组。
    }

 时间复杂度为O(n),n为数组长度。

空间复杂度为O(1),只用了常数个变量。

算法示例

以nums = [5,7,7,8,8,10], target = 8为例

第一步:初始化

left=0,right=5,ans[2]={-1,-1}

第二步:找左端点

  1. left=0,right=6,mid=left+(right-left)/2=0+(5-0)/2=2,nums[mid]=7<target=8.让left=mid+1=3

   2.此时mid=3+(5-3)/2=4,nums[mid]=target=8,让right=mid=4

 3.mid=3+(4-3)/2=3,nums[mid]=8=target,此时让right=mid=3,同时left==right,结束循环。

 

4.此时判断right位置的值是否为目标值相等,让ans[0]=right=3.

第三步:找右端点

  1. 由于在找左端点时,right此时已经走到了左端点的位置,但此时我们需要让right=nums.length-1,left=0(此处left可以不为0,从当前位置直接开始,这里为了方便观看,从0开始)
  2. mid=0+(5-0+1)/2=3,此时nums[mid]==target,让left=mid。

 3.mid=3+(5-3+1)/2=4,nums[mid]=4=target,让left=mid。

4.mid=4+(5-4+1)/2=5,nums[mid]>target=8,此时让right=mid-1.

5.此时left==right,结束循环,ans[1]=4

第四步:返回结果

此时ans={3,4},返回ans。 

搜索插入位置

算法分析

本道题是在排序数组中查找目标值,若找不到目标值,则返回插入位置。对于这道题,如果我们采用暴力遍历的解法,时间复杂度为O(n).但题目要求我们使用O(logn)的算法,那么我们可以采用二分查找的方法来解决。

算法步骤

  1. 初始化:设置left和right,left初始化为0,right为nums.length-1。
  2. 查找位置:设置mid,mid=left+(right-left)/2,当nums[mid]<target,让left=mid+1,当nums[mid]>=target时,让right=mid,为什么在等于的时候还要让right=mid?这是为了让target在数组中与target相等数的最左侧插入(才能满足题目要求)。循环条件为:left<right,(当left和right相遇,说明已经找到结果)
  3. 处理边界:当target的值比nums[left]大时,此时插入的位置为left+1。
  4. 返回结果:判断target和nums[left]的大小之后,若大于则返回left+1,反之,返回left。

算法代码

    /**
     * 在排序数组中查找目标值的插入位置。
     * 通过二分查找法,确定目标值应该插入的位置,以保持数组的有序性。
     * 
     * @param nums 排序数组,数组中的元素升序排列。
     * @param target 目标值,我们需要找到将其插入到数组中的位置。
     * @return 返回目标值应该插入的位置索引。
     *         如果目标值存在于数组中,则返回目标值的索引;
     *         如果目标值不存在于数组中,则返回目标值应该插入的位置索引。
     */
    public int searchInsert(int[] nums, int target) {
        /* 初始化左右指针 */
        int left = 0, right = nums.length - 1;
        
        /* 使用二分查找法缩小查找范围 */
        while (left < right) {
            /* 计算中间位置,避免整数溢出 */
            int mid = left + (right - left) / 2;
            
            /* 如果中间位置的值小于目标值,则目标值应该在中间位置的右侧 */
            if (nums[mid] < target) left = mid + 1;
            /* 如果中间位置的值大于等于目标值,则目标值应该在中间位置或其左侧 */
            else right = mid;
        }
        /* 检查最终的左指针位置 */
        /* 如果数组中的最后一个元素小于目标值,则目标值应该插入到数组的最后 */
        if (nums[left] < target) return left + 1;
        
        /* 如果数组中的最后一个元素大于等于目标值,则目标值已经在数组中,返回左指针位置 */
        return left;
    }

时间复杂度为O(logn),n为数组的长度

空间复杂度为O(1),只用了常数个的变量。

算法示例

以nums = [1,3,5,6], target = 5为例

第一步:初始化

left=0,right=3

第二步:找插入位置

  1. mid=left+(right-left)/2=0+(3-0)/2=1,mid=3<target=5,让left=mid+1=2

2.mid=2+(3-2)/2=2,nums[mid]=5=target=5,让right=mid。

 

3.此时left==right,结束循环。

第三、四步:判断target和nums[left]的大小,返回结果,此时target=nums[left],返回left=2

 x 的平方根

算法分析

本道题是想查找一个整数的算法平方根,若我们使用暴力遍历的方法,时间复杂度为O(N),我们可以采用二分查找的方法来进行优化。在遍历的过程中,每次缩小一般的数据量,来减少循环次数,时间复杂度为O(logN),

算法步骤

  1.  初始化:设置left和right,让left=0,right=x,(这里为了后续操作防止溢出,需要设置为long类型)。

  2. x值判断:如果x的值是小于1的,则直接返回0.

  3. 查找x的算术平方根:设置mid=left+(right-left+1)/2,为了防溢出,mid的类型也为long。当mid*mid<=x时,此时让left=mid;反之,mid*mid>x,此时让right=mid-1。

  4. 结束循环:当left和right相遇时,此时说明已经找到了x的算术平方根,返回结果即可。

算法代码

/**
     * 计算一个整数的平方根的整数部分。
     * 采用二分查找的方法来逼近平方根的值,避免了浮点数运算,提高了计算精度。
     * 
     * @param x 待求平方根的非负整数
     * @return 平方根的整数部分
     */
    public int mySqrt(int x) {
        // 如果x小于1,直接返回0,因为0和负数没有平方根
        if(x < 1) return 0;
        
        // 初始化左右边界,左边界为1,右边界为x
        long left = 1, right = x;
        
        // 使用二分查找法来逼近平方根的值
        while(left < right) {
            // 计算中间值,避免整数溢出,并确保mid为整数
            long mid = left + (right - left + 1) / 2;
            
            // 如果中间值的平方小于等于x,说明平方根的值在mid或mid的右侧
            if(mid * mid <= x) {
                left = mid;
            } else {
                // 否则,平方根的值在mid的左侧
                right = mid - 1;
            }
        }
        
        // 返回查找到的平方根的整数部分
        return (int) left;
    }

时间复杂度为O(logX),X为整数的值。

空间复杂度为O(1),只用了常数个变量。 

算法示例 

以x=4为例

第一、二步:初始化,判断x是否小于0

left=0,right=4

第三步:进行查找x的算术平方根

  1. mid=left+(right-left+1)/2=1+(4-1+1)/2=3,mid*mid=9>x=4,让right=mid-1

2.mid=1+(2-1+1)/2=2,mid*mid=4=x,让left=mid。此

第四步:返回结果

此时left=right,说明找到了x的算术平方根,返回left(需要注意强转为int)


二分查找上篇就先到这了~

若有不足,欢迎指正~

  • 17
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhyhgx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值