【优选算法 二分查找】二分查找算法模板详解:二分查找 & 在排序数组中查找元素的第一个和最后一个位置
二分查找
题目描述
题目解析
暴力解法
我们可以从左往右遍历一次数组,如果存在 target 则返回数组的下标,否则返回 -1;
时间复杂度 O(N),因为没有利用数组有序的特点,每次比较只能舍弃一个要比较的数;
二分查找法
所以我们可以把 [1 , 4 ] 这个区间舍弃掉,直接看 [ 5 , 8 ];仅仅拿 4 和 5 比较一次,就干掉了一大片数;
总结
在一个数组中,随便找一个数,拿这个数和 target 进行比较,并且以拿的这个数分成两个区间,比较后舍弃一部分数,然后再从另一个区间的数中,找下一个要和 target 比较的数;
二分查找算法的本质,就是当数组具有“二段性”时,哪怕这个数组是无序的,只要能在这个数组中发现“二段性”,那么也可以使用二分查找;
“二段性”
当我们发现一个规律,然后根据这个规律,选取某一个点,根据这个点,能把数组分成两个区域,根据规律能够有选择地舍弃一部分,进而在另一个部分继续查找的时候,此时,就可以使用二分查找的算法
我们要从数组中找和 target 进行比较的数,不一定是先从 n/2 的位置开始找,只要找的这个数的点,能把这个区间分成两部分,即满足二段性,进而使用二分查找法;
但是哪怕那么多点可以选择,但是我们都应该先选择 n/2 的位置的点,因为在概率学的数学期望中,我们选择中间的点,时间复杂度是最好的;
- nums[mid] < target,left = mid+1
- nums[mid] = target,return mid
- nums[mid] > target,right = mid-1
细节问题
因为当 left = right 的时候,也是要继续判断的,所以 left <= right 为循环判断条件;
时间复杂度,只需要看循环执行多少次:
如何求 x 呢?
int mid = (left + right) / 2,mid 会有溢出风险:
如果想不从 n/2 开始查找,可以修改,如下面的写法是从 n/3 开始查找的:
在排序数组中查找元素的第一个和最后一个位置
题目解析
发现二段性
方法一:利用数组有序特性,使用简单的二分查找法
如果使用简单的二分查找,创建左指针和右指针,再找中间值,对这个中间值分别进行讨论;如果对于一些特点的情况,如下面的这种情况,这个方法的时间复杂度会非常高;
虽然 mid 指针刚好指向 target,但是不能确定此时的 mid 指向的是在目标连续区域的哪一个位置,从而不好找目标区域的起始位置和终点位置;
所以使用简单二分查找的方法,对于上述情况和类似情况,查找的时间复杂度会近似于暴力查找;
方法二:在简单二分查找方法的基础上,对二分策略进行优化
回到题目的规律:当发现题目具有二段性,就可以利用二分查找;
因为不能同时查找目标区间起始位置和终点位置,所以我们可以先查找起始位置;
利用二段性,我们可以根据目标值起始位置,把数组区间分成两个部分;
左边区域的值都是小于 target,右边区域的值都大于等于target,根据 target,在查询目标区域左端点的时候,发现这个数组是有二段性,因此可以使用二分查找:
查找区间左端点
以 mid 的值来决定 left 和 right 的更新方法
我们设 mid 指向的值为 x,我们要找 >=target 区间的起始位置,拿 x 的值与 target 讨论;
- 1. x < target,此时 mid 落在 < target 的区域,不可能有最终结果,所以继续往下执行:
- 2. x >= target
(不同点,之前简单二分查找的方法分三种情况讨论,这里只分两种情况,因为 x= target 并不是最终的结果,因为最终的结果的左端点和右端点)
对于这种情况,对应的做法:
我们虽然[mid , right] 区间的情况一定是都大于 target,但是不知道 [left , mid] 区间的具体情况是都小于 target 还是部分小于 target;所以 right 绝对不能移动到 [left , mid] 这个不确定的区间,因为如果 mid 在修改之前就是目标区间的左端点,让 right = mid -1,[left , right]这个区间就没有 target 了,所以 right = mid 即可;
处理细节问题
1.循环条件
对于执行循环操作(二分查找操作)的第一个循环条件,是 left <= right,还是 left < right 呢?
一定是选择 left< right ,为什么呢?有两个原因;
- 原因一:因为 left = right 的情况不需要放到循环中判断
我们分三种情况来讨论(ret 为返回的最终结果):
- 1. 数组中有元素等于 target
前面提到,left 刚开始一直处于 < target 的区间,在 x>= target 的条件下,right=mid,所以right 一定在 ret 右边的区间,threshold 为ret (第一个 target 元素)的前一个元素;
因为 left 和 right 的调整方式是 left = mid+1,right = mid,所以 right 一直在>= target 的区域移动,而 left = mid +1,说明 left 是一直想要跳出 < target 的区域的;
所以当 left 和 right 调整到最后,循环结束,此时 left 一定会和 right 相等,此时我们在循环结束后单独判断相遇点和 target 是否相等即可;如果 left = right ,那么两个指针指向的位置,一定就是目标区间左端点 ret,所以循环条件只需要判断 left < right 即可;
- 2. 数组中所有元素全都大于 target
如果所有数组元素都大于 target,那么整个过程只有 right 指针在不断向左边移动,直到 left 和 right 相遇;
哪怕相遇,也没有最终结果,所以这种情况下,我们只需要在left < right 时执行循环,循环退出,left=right;
此时我们只需要拿当前两个指针指向的值和 target 比较:相同则回到第一种情况,这个值就是目标区间的左端点;不相同则返回 [-1,-1];
- 3. 数组中所有元素全都小于 target
这种情况和第二种情况同理:
整个更新过程都只有 left 在移动,循环退出时,left = right,只需要判断当前指针的值和 target 是否相等即可,相等则这个值就是目标区域的起始位置 ret,否则返回 [-1,-1];
- 原因二:如果在循环条件中判断 left = right,会出现死循环
什么时候会出现死循环,就是整个数组中有元素等于 target 的时候:
如果循环条件是 while(left <= right){ ..... },此时left = right ,更新的 mid还是指向 left,right 所在位置,此时满足 x <= target 条件,right=mid,所以 right = left,left 和 right 就会一直停在一个地方循环更新结果,这种情况下就会出现死循环;
2. 求中点操作
求中点操作,也就是在定义 left 和 right 之后,求 mid 的操作;
- 1. mid = left + ( right - left ) / 2 ;
- 2. mid = left +( right - left +1 ) /2 ;
这两种求中点的操作,区别在于数组元素是偶数时,更新 mid 的位置不同;这两种方法在简单二分情况,如第一题的情况都是可以用的,但是这种情况下不能用 mid = left + (right - left +1)/2 求中点;
如果是用第二种方法 mid = left + (right - left +1)/2,此时 mid 的位置:
- 如果上图条件中,target > 3,那么此时 mid < target ,left = mid+1,程序结束,此时是没问题的;
- 但是,如果 target <=3,那么此时 mid>=target,此时调整 right = mid,就又进入死循环了:
所以,在求左端点时,只能用 mid = left +(right - left )/2 来求中点;
查找区间右端点
根据查找右端点,依旧可以把整个区间分成两部分,<=target,>target :
根据 mid 与 target 的关系决定更新操作
1. x <= target
因为我们要保证在调整 left 和 right 的过程中,右端点一定要在 [left,right] 区间,所以当前这种情况,我们应该移动 left:
2. x > target
因为 x > target,所以mid 所在位置一定是在目标区域之外,因此更新 right = mid -1;
处理细节问题
1. 循环条件
2. 求中点的操作
循环条件和求左端点相同,不同的是求中点的公式
- 1. mid = left + ( right - left ) / 2 ;
- 2. mid = left +( right - left +1 ) /2 ;
这两种求中点的操作,区别在于数组元素是偶数时,更新 mid 的位置不同;这两种方法在简单二分情况,如第一题的情况都是可以用的,但是这种情况下不能用 mid = left + (right - left )/2 求中点;
为了接下来的操作不和求左端点的时候弄混,要牢记此时要求的是右端点,接下来的假设都是为了能求出右端点:
如果是用第二种方法 mid = left + (right - left )/2,此时 mid 的位置:
- 如果上图条件中,target < 2,那么此时 mid > target ,right = mid-1,程序结束,此时是没问题的;
- 但是,如果 target >=3,那么此时 mid>=target,此时调整 left = mid,就又进入死循环了:
所以,在求右端点时,只能用 mid = left +(right - left +1 )/2 来求中点,在求右端点时,为什么用这条公式求中点不会出现死循环呢?
用 mid = left +(right - left +1 )/2 来求中点,此时 mid 在 right 的位置,如果此时 x > target,right=mid-1,此时不满足 left<right 的条件,会结束循环;如果 x<= target,left = mid :
此时也不满足 left<right 的条件,也会结束循环,所以在求右端点的时候,用 mid = left +(right - left +1)/2 来求中点,就不会出现死循环;
编写代码
总结二分模板
相关文章:

【优选算法 二分查找】二分查找算法模板详解:二分查找 & 在排序数组中查找元素的第一个和最后一个位置
二分查找 题目描述 题目解析 暴力解法 我们可以从左往右遍历一次数组,如果存在 target 则返回数组的下标,否则返回 -1; 时间复杂度 O(N),因为没有利用数组有序的特点,每次比较只能舍弃一个要比较的数&…...

gitlab 生成并设置 ssh key
一、介绍 🎯 本文主要介绍 SSH Key 的生成方法,以及如何在GitLab上添加SSH Key。GitLab 使用SSH协议与Git 进行安全通信。当您使用 SSH密钥 对 GitLab远程服务器进行身份验证时,您不需要每次都提供您的用户名和密码。SSH使用两个密钥&#x…...

计算机视觉在科学研究(数字化)中的实际应用
计算机视觉是一种利用计算机技术来解析和理解图像和视频的方法。.随着计算机技术的不断发展,计算机视觉被广泛应用于科学研究领域,为科学家提供了无限的可能。 一、生命科学领域 在生命科学领域,计算机视觉被广泛用于图像识别、分类和测量等…...

移动应用开发课程第六次实验:为实验2添加登陆页面,用SQList存储好友基本信息
1、在Android Studio中,请在第二次实验成果的基础上完成以下实验要求。 向右滑动 请添加登录页面。在登录页面中,如果用户输入的用户名和密码正确,则跳转至如上图所示的好友列表,并记录用户的登录信息,在用户第一次登…...

nextjs增加系统路径前缀(basePath)适配方案
在 Next.js 中,路由是通过文件夹结构来定义的,使用类似于 History 模式的 URL 结构。所以如果想通过nginx来代理一个nextjs开发的系统, 除非直接使用跟路径/来进行代理,否则代理将非常麻烦,这时添加basePath就非常有必…...

嵌入式蓝桥杯学习拓展 LCD翻转显示
通过配置SS和GS两个标志位,实现扫描方向的切换。 将lcd.c的REG_932X_Init函数进行部分修改。 将LCD_WriteReg(R1, 0x0000);修改为LCD_WriteReg(R1,0x0100); 将LCD_WriteReg(R96, 0x2700); 修改为LCD_WriteReg(R96, 0xA700); void REG_932X_Init1(void) {LCD_Wr…...

学习threejs,实现配合使用WebWorker
👨⚕️ 主页: gis分享者 👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨⚕️ 收录于专栏:threejs gis工程师 文章目录 一、🍀前言1.1 ☘️WebWorker web端多线程 二、…...

TDengine 新功能 复合主键
1. 简介 从 TDengine 3.3.0.0 版本之后,新增了复合主键的功能。 TDengine 原来的时间列是不允许有重复时间戳的,有了复合主键功能后,时间列即允许有重复,重复后的时间戳按紧跟其后第二列主键列的值来确定唯一性。 此功能的常用…...

JVM 面试题
Java 虚拟机(JVM)是运行 Java 程序的引擎,它是 Java 语言 “一次编译,处处运行” 的核心技术。JVM 的主要任务是将 Java 字节码(Bytecode)解释成机器码并执行,负责内存管理、线程管理、垃圾回收…...

组件上传图片不回显问题
import { Plus } from "element-plus/icons-vue"; // 图片上传 const img_add ref([]); function httpRequest_add(option) {let dataForm new FormData();dataForm.append("file", option.file);dataForm.append("id", user.data.id);axios({…...

【JavaWeb后端学习笔记】Spring AOP面向切面编程
AOP 1、Spring AOP概述2、SpringAOP快速入门3、SpringAOP核心概念4、通知类型5、通知顺序6、切入点表达式6.1 execution方式6.2 annotation方式 7、连接点 1、Spring AOP概述 AOP:Aspect Oriented Programming,面向特定方法编程。 AOP是通过动态代理技术…...

6.584-Lab5B
6.584-Lab5B Reference CodeReference BlogHomeworkMyself Code Sharded Key/Value Service 梗概 这个图是我从上面参考blog中拿来的,觉得做的不错,借助这张图来讲解一下需要一个什么样的 Service。 ShardCtrler Client: 接收来自客户发出的命…...

OceanBase 的探索与实践
作者:来自 vivo 互联网数据库团队- Xu Shaohui 本文总结了目前我们遇到的痛点问题并通过 OceanBase 的技术方案解决了这些痛点问题,完整的描述了 OceanBase 的实施落地,通过迁移到 OceanBase 实践案例中遇到的问题与解决方案让大家能更好的了…...

安卓调试环境搭建
前言 前段时间电脑重装了系统,最近准备调试一个apk,没想到装环境的过程并不顺利,很让人火大,于是记录一下。 反编译工具下载 下载apktool.bat和apktool.jar 官网地址:https://ibotpeaches.github.io/Apktool/install…...

动画Lottie
Lottie简介 Lottie是一个Airbnb 开发的用于Android,iOS,Web和Windows的库,用于解析使用Bodymovin导出为json的Adobe After Effects动画,并在移动设备和网络上呈现 — GitHub Lottie主要特性 After Effects 兼容性: …...

C++感受14-Hello Object 封装版 - 上
1. 封装即约束——封装和派生、多态的本质区别 一门计算机语言,要如何帮助程序员写出优秀的代码?两个方法:一是给程序员更多能力,二是给程序员更多约束。之前我们学习的派生和多态,更多的是给我们技能,而封…...

网络安全中大数据和人工智能应用实践
传统的网络安全防护手段主要是通过单点的网络安全设备,随着网络攻击的方式和手段不断的变化,大数据和人工智能技术也在最近十年飞速地发展,网络安全防护也逐渐开始拥抱大数据和人工智能。传统的安全设备和防护手段容易形成数据孤岛࿰…...

RISC-V架构下OP-TEE 安全系统实践
安全之安全(security)博客目录导读 本篇博客,我们聚焦RISC-V 2024中国峰会上的RISC-V和OP-TEE结合的一个安全系统实践,来自芯来科技桂兵老师。 关于RISC-V TEE(可信执行环境)的相关方案,如感兴趣可参考R...

40分钟学 Go 语言高并发:【实战】分布式缓存系统
【实战课程】分布式缓存系统 一、整体架构设计 首先,让我们通过架构图了解分布式缓存系统的整体设计: 核心组件 组件名称功能描述技术选型负载均衡层请求分发、节点选择一致性哈希缓存节点数据存储、过期处理内存存储 持久化同步机制节点间数据同步…...

[创业之路-186]:《华为战略管理法-DSTE实战体系》-1-为什么UTStarcom死了,华为却活了,而且越活越好?
目录 前言 一、市场定位与战略选择 二、技术创新能力 三、企业文化与团队建设 四、应对危机的能力 五、客户为中心的理念 六、市场适应性与战略灵活性 七、技术创新与研发投入 八、企业文化与团队建设 九、应对危机的能力 前言 UT斯达康(UTStarcom&#…...

python如何多行注释
在Python中,多行注释通常有两种方式: 使用三个单引号()或三个双引号(""")来创建多行字符串,这可以被用来作为多行注释。这种方式在Python中实际上是创建了一个多行的字符串对象…...

前端工程化面试题目常见
前端工程化面试常见题目包括: • 谈谈你对WebPack的认识。 • Webpack打包的流程是什么? • 说说你工作中几个常用的loader。 • 说说HtmlWebpackPlugin插件的作用。 • Webpack支持的脚本模块规范有哪些? • Webpack和gulp/grunt相比有什么特…...

定点数的乘除运算
原码一位乘法 乘积的符号由两个数的符号位异或而成。(不参与运算)被乘数和乘数均取绝对值参与运算,看作无符号数。乘数的最低位为Yn: 若Yn1,则部分积加上被乘数|x|,然后逻辑右移一位;若Yn0&…...

页面置换算法模拟 最近最久未使用(LRU)算法
最近最久未使用(LRU)算法是一种基于页面访问历史的页面置换算法。它选择最久未使用的页面进行置换。当需要访问一个不在内存中的页面时,如果内存已满,则选择最久未使用的页面进行置换。LRU算法通过记录页面的访问时间戳来判断页面…...

Ubuntu与Centos系统有何区别?
Ubuntu和CentOS都是基于Linux内核的操作系统,但它们在设计理念、使用场景和技术实现上有显著的区别。以下是详细的对比: 1. 基础和发行版本 Ubuntu: 基于Debian,使用.deb包管理系统。包含两个主要版本: LTSÿ…...

RK3568平台开发系列讲解(pinctrl 子系统篇)pinctrl_debug
🚀返回专栏总目录 文章目录 1. Overview2. debug信息2.1 pinctrl-devices2.2. pinctrl-handles2.3. pinctrl-handles3. debug信息3.1. 查看(pinctrl_register_pins)注册了哪些pins3.2. 查看pin groups;3.3. 查看每种functions所占用的gpio groups信息:3.4. pinconf沉淀、…...

避大坑!Vue3中reactive丢失响应式的问题
在vue3中,我们定义响应式数据无非是ref和reactive。 但是有的小伙伴会踩雷!导致定义的响应式丢失的问题。 reactive丢失响应式的情况1(直接赋值) 场景: 1.你定义了一个数据:let datareactive({name:"",age:"" }) 2.然后你…...

springSecurity权限控制
权限控制:不同的用户可以使用不同的功能。 我们不能在前端判断用户权限来控制显示哪些按钮,因为这样,有人会获取该功能对应的接口,就不需要通过前端,直接发送请求实现功能了。所以需要在后端进行权限判断。࿰…...

Pytorch训练固定随机种子(单卡场景和分布式训练场景)
模型的训练是一个随机过程,固定随机种子可以帮助我们复现实验结果。 接下来介绍一个模型训练过程中固定随机种子的代码,并对每条语句的作用都会进行解释。 def seed_reproducer(seed2333):random.seed(seed)os.environ["PYTHONHASHSEED"] s…...

Conda + JuiceFS :增强 AI 开发环境共享能力
Conda 是当前 AI 应用开发领域中非常流行的环境和包管理系统,因其能够简单便捷地创建与系统资源相隔离的虚拟环境广受欢迎。 Conda 支持在不同的操作系统上重建相同的工作环境,但在环境共享复用方面仍存在一些挑战。比如,在不同机器上复用相…...