当前位置: 首页 > news >正文

【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,右边界为xlong 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)


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

若有不足,欢迎指正~

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • sortBy排序操作
  • 【JavaScript】01数组原型对象的最后一个元素、计数器
  • 【精通Redis】Redis命令详解
  • clangd配置
  • vue - devtools 安装
  • MacOS DockerDesktop配置文件daemon.json的位置
  • MakeReal3D v5.0 爆炸视图
  • 基于springboot+vue+uniapp的校园二手交易小程序
  • K8s大模型算力调度策略的深度解析
  • 使用 AI 支持的元描述生成器提升SEO效果
  • Bugku-ctf-web-eval
  • C# 调用Webservice接口接受数据测试
  • hcip学习 DHCP中继
  • 防洪评价报告编制方法与水流数学模型建模技术
  • mysql+php+html实现学生管理系统
  • 【Leetcode】101. 对称二叉树
  • 【347天】每日项目总结系列085(2018.01.18)
  • 【5+】跨webview多页面 触发事件(二)
  • 【跃迁之路】【641天】程序员高效学习方法论探索系列(实验阶段398-2018.11.14)...
  • Java 11 发布计划来了,已确定 3个 新特性!!
  • JavaScript设计模式与开发实践系列之策略模式
  • JAVA之继承和多态
  • NSTimer学习笔记
  • Redis中的lru算法实现
  • vue-loader 源码解析系列之 selector
  • 安卓应用性能调试和优化经验分享
  • -- 查询加强-- 使用如何where子句进行筛选,% _ like的使用
  • 跳前端坑前,先看看这个!!
  • 扩展资源服务器解决oauth2 性能瓶颈
  • ​LeetCode解法汇总2670. 找出不同元素数目差数组
  • ​水经微图Web1.5.0版即将上线
  • (22)C#传智:复习,多态虚方法抽象类接口,静态类,String与StringBuilder,集合泛型List与Dictionary,文件类,结构与类的区别
  • (day6) 319. 灯泡开关
  • (PySpark)RDD实验实战——取最大数出现的次数
  • (Redis使用系列) SpringBoot中Redis的RedisConfig 二
  • (二)hibernate配置管理
  • (附源码)ssm考试题库管理系统 毕业设计 069043
  • (附源码)基于SpringBoot和Vue的厨到家服务平台的设计与实现 毕业设计 063133
  • (四)Controller接口控制器详解(三)
  • (四)React组件、useState、组件样式
  • (四)模仿学习-完成后台管理页面查询
  • (续)使用Django搭建一个完整的项目(Centos7+Nginx)
  • (一)appium-desktop定位元素原理
  • ..thread“main“ com.fasterxml.jackson.databind.JsonMappingException: Jackson version is too old 2.3.1
  • .net core 管理用户机密
  • .NET MAUI Sqlite程序应用-数据库配置(一)
  • .NET 简介:跨平台、开源、高性能的开发平台
  • .net 连接达梦数据库开发环境部署
  • .net2005怎么读string形的xml,不是xml文件。
  • .net经典笔试题
  • [ C++ ] STL_stack(栈)queue(队列)使用及其重要接口模拟实现
  • [ IO.File ] FileSystemWatcher
  • [.net] 如何在mail的加入正文显示图片
  • [2024-06]-[大模型]-[Ollama] 0-相关命令
  • [ACM独立出版]2024年虚拟现实、图像和信号处理国际学术会议(ICVISP 2024)