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

news2025/2/23 18:29:04

目录

 前言

什么是二分查找?

二段性

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

 算法分析

算法步骤

 算法代码

算法示例

模板

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

算法分析

算法步骤

算法代码

算法示例

搜索插入位置

算法分析

算法步骤

算法代码

算法示例

 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)


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

若有不足,欢迎指正~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1950682.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HTML常见标签——超链接a标签

一、a标签简介 二、a标签属性 href属性 target属性 三、a标签的作用 利用a标签进行页面跳转 利用a标签返回页面顶部以及跳转页面指定区域 利用a标签实现文件下载 一、a标签简介 <a>标签用于做跳转、导航&#xff0c;是双标签&#xff0c;记作<a></a>&#…

MFC开发,自定义消息

在MFC开发中&#xff0c;主要核心机制就是消息机制。QT与之类似的机制就是信号与槽。QT中的信号与槽是非常容易自定义的&#xff0c;MFC也是如此&#xff0c;自定义也是比较方便&#xff0c;况且自定义消息或者控件在整个GUI图形化界面开发中也是非常重要的部分&#xff0c;上篇…

项目都做完了,领导要求国际化????--JAVA后端篇

springboot项目国际化相信各位小伙伴都会&#xff0c;很简单&#xff0c;但是怎么项目都做完了&#xff0c;领导却要求国际化文件就很头疼了 国际化的SpringBoot代码&#xff1a; 第一步&#xff1a;创建工具类 /*** 获取i18n资源文件** author bims*/ public class Message…

Java Collections类

Collections是一个与有关集合的工具类&#xff0c;提供了很多对集合进行操作的方法。 常见方法 addAll&#xff1a;往集合中添加多个元素。 public static <T> boolean addAll(Collection<? super T> c, T... elements) {boolean result false;for (T element…

MongoDB教程(二十):MongoDB正则表达式

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; 文章目录 引言一、正则表…

Python3网络爬虫开发实战(3)网页数据的解析提取

文章目录 一、XPath1. 选取节点2. 查找某个特定的节点或者包含某个指定的值的节点3. XPath 运算符4. 节点轴5. 利用 lxml 使用 XPath 二、CSS三、Beautiful Soup1. 信息提取2. 嵌套选择3. 关联选择4. 方法选择器5. css 选择器 四、PyQuery1. 初始化2. css 选择器3. 信息提取4. …

程序的机器级表示(一)汇编,汇编格式和数据传输指令

系列文章 : 深入理解计算机系统笔记 文章目录 系列文章3 程序的机器级表示3.1 历史观点3.2 程序编码3.2.1 机器级代码3.2.2 代码示例3.2.3 关于格式的注解 3.3 数据格式3.4 访问信息3.4.1 操作数指示符3.4.2 数据传送指令3.4.3 数据传送示例3.4.4 压入和弹出栈数据 3 程序的机…

如何做校园圈子小程序,需要哪些功能?可打包APP小程序H5,源码交付,支持二开!

独立学校首页 支持每个学校独立首页!每个学校都可以拥有专属首页&#xff0c;打造不同风格的学校首页展示效果 多业务覆盖 可实现校园内外卖、跑腿、超市、药店水果、快餐店等业务全覆盖!所有配送业务平台都可开展 多站点运营 支持多学校多站点运营&#xff0c;各分站管理员可独…

【日记】办个护照不至于有这种刑事罪犯一样的待遇吧……(737 字)

正文 暴晒&#xff0c;中午出去骑共享单车&#xff0c;座垫都不敢坐。 至于为什么&#xff0c;中午觉都不睡跑出去&#xff0c;是因为今天他们办承兑汇票的业务&#xff0c;搞了一天&#xff0c;中午不休息&#xff0c;说可能还会用到我的指纹&#xff0c;让我 on call。我心想…

基础IO(文件系统)

一、块组的宏观理解 1、分区和分组 首先一台电脑就一个磁盘&#xff0c;一般800GB到1TB&#xff0c;为了管理这么大的内存数据&#xff0c;我们就对磁盘进行分区&#xff0c;分区之后才是我们看到的C盘&#xff0c;D盘等。 但是其实分区之后空间还是太大不好管理&#xff0c;…

习题2.23

不解释了&#xff0c;这么简单的题目。 (defn for-each[f item](if (not (empty? item));(println (f (first item)))(f (first item)))(if (not (empty? item))(for-each f (rest item)));(f (first item)))执行结果如下

FastGPT、Dify、Coze产品功能对比分析

在当前的人工智能领域&#xff0c;模型接入、应用发布、应用构建、知识库和工作流编排等功能是衡量一个AI平台综合能力的重要指标。本文将对FastGPT、Dify和Coze这三款产品的功能进行详细对比分析&#xff0c;以帮助用户更好地了解它。 订阅模式及市场概况 在订阅模式及市场概…

CSS常见属性详解——内边距与外边距

内边距与外边距 内边距 外边距 应用场景 在网页排版布局时&#xff0c;我们经常会希望元素与元素之间有一定的间距&#xff0c;此时我们可能会用到CSS的外边距或内边距属性&#xff0c;这两个属性都能让元素之间产生距离&#xff0c;那么他们之间有什么不同呢&#xff1f; …

【SpringBoot】1 Gitee

本项目 Gitee 地址&#xff1a;https://gitee.com/Lin_DH/system idea中可能装个gitee的插件&#xff0c;这样操作起来比较方便。 1&#xff09;登录 Gitee 官网&#xff08;https://gitee.com/&#xff09;&#xff0c;新建仓库。 2&#xff09;复制新建的 Gitee 仓库地址&am…

大麦抢票-狠货

大部分购买方式已迁移至手机端&#xff0c;专注研究移动端 小白操作–仅供学习 注意在帐号按权重的第三方账号设置解绑淘宝&#xff0c;否则有可能在抢票时候出现滑块&#xff0c;影响抢票,抢票优先选择大麦 ⚠️核心内容参考&#xff1a; 据悉&#xff0c;在购票环节&…

【2024最新华为OD-C/D卷试题汇总】[支持在线评测] LYA的朋友排队(100分) - 三语言AC题解(Python/Java/Cpp)

🍭 大家好这里是清隆学长 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 💻 ACM银牌🥈| 多次AK大厂笔试 | 编程一对一辅导 👏 感谢大家的订阅➕ 和 喜欢💗 🍿 最新华为OD机试D卷目录,全、新、准,题目覆盖率达 95% 以上,支持题目在线…

深入分析 Android ContentProvider (五)

文章目录 深入分析 Android ContentProvider (五)ContentProvider 的性能优化和实践案例1. 性能优化技巧1.1. 数据库索引优化示例&#xff1a;添加索引 1.2. 批量操作与事务管理示例&#xff1a;批量插入操作 1.3. 使用异步操作示例&#xff1a;使用 AsyncTask 进行异步查询 1.…

FastAPI(七十八)实战开发《在线课程学习系统》接口开发-- 评论

源码见&#xff1a;"fastapi_study_road-learning_system_online_courses: fastapi框架实战之--在线课程学习系统" 梳理下思路 1.判断是否登录 2.课程是否存在 3.如果是回复&#xff0c;查看回复是否存在 4.是否有权限 5.发起评论 首先新增pydantic模型 class Cour…

如何为 DigitalOcean 上的托管数据库收集可观测指标

DigitalOcean 在 2024 年 5 月开始支持在托管数据库&#xff08;PostgreSQL、MySQL、Redis和Kafka&#xff09;中收集可观测指标。我们将在本偏内容中&#xff0c;告诉大家如何使用部署在 DigitalOcean App Platform 上的网络应用程序&#xff0c;为 DigitalOcean 上的 Postgre…

C语言 | Leetcode C语言题解之第283题移动零

题目&#xff1a; 题解&#xff1a; void swap(int *a, int *b) {int t *a;*a *b, *b t; }void moveZeroes(int *nums, int numsSize) {int left 0, right 0;while (right < numsSize) {if (nums[right]) {swap(nums left, nums right);left;}right;} }