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

目录

前言

什么是二分查找?

二段性

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

算法分析

算法步骤

算法代码

算法示例

模板

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

算法分析

算法步骤

算法代码

算法示例

搜索插入位置

算法分析

算法步骤

算法代码

算法示例

[x 的平方根](#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即可。

算法代码

java 复制代码
/**
     * 在排序数组中查找目标值的索引。
     * 该方法使用二分查找算法,在一个已排序的数组中查找目标值的索引。如果目标值存在于数组中,则返回其索引;
     * 如果不存在,则返回-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即可。

模板

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

java 复制代码
 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.

算法代码

java 复制代码
    /**
     * 在排序数组中查找给定目标值的起始和结束位置。
     *
     * @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。

算法代码

java 复制代码
    /**
     * 在排序数组中查找目标值的插入位置。
     * 通过二分查找法,确定目标值应该插入的位置,以保持数组的有序性。
     * 
     * @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的算术平方根,返回结果即可。

算法代码

java 复制代码
/**
     * 计算一个整数的平方根的整数部分。
     * 采用二分查找的方法来逼近平方根的值,避免了浮点数运算,提高了计算精度。
     * 
     * @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)


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

若有不足,欢迎指正~

相关推荐
JavaGuide2 分钟前
JDK 25(长期支持版) 发布,新特性解读!
java·后端
用户3721574261353 分钟前
Java 轻松批量替换 Word 文档文字内容
java
白鲸开源4 分钟前
教你数分钟内创建并运行一个 DolphinScheduler Workflow!
java
CoovallyAIHub38 分钟前
中科大DSAI Lab团队多篇论文入选ICCV 2025,推动三维视觉与泛化感知技术突破
深度学习·算法·计算机视觉
Java中文社群1 小时前
有点意思!Java8后最有用新特性排行榜!
java·后端·面试
代码匠心1 小时前
从零开始学Flink:数据源
java·大数据·后端·flink
间彧1 小时前
Spring Boot项目中如何自定义线程池
java
间彧1 小时前
Java线程池详解与实战指南
java
用户298698530141 小时前
Java 使用 Spire.PDF 将PDF文档转换为Word格式
java·后端
NAGNIP2 小时前
Serverless 架构下的大模型框架落地实践
算法·架构