二分搜索的几种写法与常见问题

最近在比赛和刷题的时候经常遇到二分答案的题,但时不时会因为一些细节上的错误而浪费时间,本文旨在整理常见的二分搜索的写法、二分搜索可能会遇到的一些小问题,以及 C++ 中与二分搜索相关的库函数,以免今后再犯类似的错误。

二分搜索的写法

查找某个值的下标

定义函数 binarySearch(nums, target) 为搜索有序数组 nums 中是否存在 i 使得 nums[i] == target,如果是,返回 i,否则返回 -1.

int binarySearch(vector<int>& nums, int target) {int low = 0, high = (int)nums.size() - 1;while (low <= high) {int mid = low + (high - low) / 2;if (nums[mid] == target) {return mid;}else if (nums[mid] > target) {high = mid - 1;}else {low = mid + 1;}}return -1;
}

这应该是大部分人最早接触的二分形式,也是最简单、最好理解的二分写法,但如果 nums 中元素存在重复的情况,并且我们需要在 nums 中存在多个 i 使得 nums[i] == target 时返回最小的 i,这种写法就失效了,而这种情况往往就是解决大部分有关二分搜索的算法问题时会遇到的。

查找左边界

要想成功实现对边界的查找,就需要对二分搜索的过程有一个更为深入的理解。还是采用与上面类似的写法,初始时将区间左右边界初始化为 low = 0, high = n - 1;. 在定义左右边界时我们应该注意到,待搜索的区间范围为 lowhigh,但是由于 lowhigh 本身就有可能为所要查找的最终结果 i,因此搜索目标位于闭区间 [low, high] 内,实际上区间内的数据分布情况我们是不得而知的,而我们已经获取的信息其实是区间外的信息,即:

  • i <= low - 1 时,nums[i] < target.
  • i >= high + 1 时,nums[i] >= target.

以上信息即为二分搜索过程中的循环不变量。需要注意的是,当 lowhigh 本身就位于左右边界的情况下, low - 1high + 1 已经超出数组范围,但由于 nums 是一个有序数组,因此我们可以这样考虑:nums[-1] = -∞, nums[n] = +∞. 因此上述的循环不变量在二分搜索开始时也满足。而要使得循环不变量在整个二分搜索过程中均满足,就需要在得到区间中点 mid 后,严格按照上述规则来更新区间左右端点:

  • nums[mid] >= target 时,要使得 nums[high + 1] >= target,那么可令 high + 1 = mid,等价于 high = mid - 1.
  • nums[mid] < target 时,要使得 nums[low - 1] < target,令 low - 1 = mid,等价于 low = mid + 1.

最终,循环条件为该闭区间不为空,表示仍然存在未确定的区间外信息,即 low <= high(取等号是因为当 low == high 时,闭区间内仍然有一个元素,应该继续循环),当退出循环时满足 low == high + 1,此时根据上述的循环不变量可知,nums[low - 1] == nums[high] < targetnums[high + 1] == nums[low] >= target,即 nums[low] 为有序数组 nums 中第一个大于等于 target 的值。

依据以上区间边界初始化方法、边界更新方法以及最终的返回值,可以很容易地编写相应代码。

// 闭区间型
int lower_bound(vector<int>& nums, int target) {int low = 0, high = (int)nums.size() - 1;while (low <= high) {int mid = low + (high - low) / 2;if (nums[mid] >= target) {high = mid - 1;}else {low = mid + 1;}}return low;
}

当然,除了以上“闭区间型”写法外,还有“左闭右开型“和开区间型,这些写法的本质思想是完全一样的,只不过是选取的循环不变量不同。

// 左闭右开型
int lower_bound(vector<int>& nums, int target) {int low = 0, high = (int)nums.size();while (low < high) { // 左闭右开区间当 low == high 时就已为空int mid = low + (high - low) / 2;// 循环不变量// nums[high] >= target// nums[low - 1] < targetif (nums[mid] >= target) {high = mid;}else {low = mid + 1;}}return low;
}
// 开区间型
int lower_bound(vector<int>& nums, int target) {int low = -1, high = (int)nums.size();while (low + 1 < high) { // 开区间当 low + 1 == high 时就已为空int mid = low + (high - low) / 2;// 循环不变量// nums[high] >= target// nums[low] < targetif (nums[mid] >= target) {high = mid;}else {low = mid;}}return high;
}

查找右边界

要对右边界进行查找,同样可以通过改写循环不变量来实现。不过通常对于元素类型为整型的有序数组来说,对右边界的查找可以转化为对左边界的查找。

比如要查找整型有序数组 nums 中最小的 i 满足 nums[i] > target,记为 upper_bound(nums, target),由于对于整型来说,nums[i] > targetnums[i] >= target + 1 等价,因此可以直接查找最小的 i 满足 nums[i] >= target + 1 ,得到的 i 即为最小的 i 满足 nums[i] > target,即 upper_bound(nums, target) = lower_bound(nums, target + 1).

同样的,诸如 nums[i] < targetnums[i] <= target 等等问题都可以通过类似的思想进行等价,这里就不过多赘述。

常见的问题

在采用二分答案法解决一些最优化问题时,上下界的确定往往是比较困难和繁琐的。但由于进行一次二分搜索的时间复杂度为 O(logn)n 的大小对最终时间的影响不会很大,因此实际面对这些问题时,往往直接令下界为 low = 0, high = INT32_MAX,但是这样又很容易出现整型溢出的问题,尤其是采用 mid = (low + high) / 2 这种写法的情况下,虽然 lowhigh 的值均位于 [0, INT32_MAX] 之间,但 low + high 却可能大于 INT32_MAX,从而导致一些意料不到的错误出现。因此求区间中值比较好的写法是 mid = low + (high - low) / 2,其在数学上与上述计算方式等价,但却可以很好地规避掉整型溢出的问题。

此外,有些问题还需要 mid 参与一些运算,来进行该问题的最优化判定,这时一个接近溢出的整数在进行一些加法或乘法运算后很容易因此溢出。因此一个比较安全的做法是将区间上下界以及区间中值都定义为 64 位整型(C++ 中为 long long 类型)。

相关库函数

  • lower_bound(first, last, value, comp);

first, last 为搜索数组的左闭右开区间,通常直接取 first = nums.begin(), last = nums.end()value 为要与元素比较的值,comp谓词函数,与排序等算法的谓词函数类似,即第一参数先序于第二参数时,返回 true,否则返回 false.

该函数的返回值为指向范围 firstlast 之间的首个不满足元素值 element < value 或者 comp(element, value) 的元素的迭代器,如果找不到,则返回 last.

  • upper_bound(first, last, value, comp);

参数与 lower_bound 相同,而返回值为指向范围 firstlast 之间的首个满足元素值 element > value 或者 comp(value, element) 的元素的迭代器,如果找不到,则返回 last.

这里只是简单的介绍了一下两种二分操作的参数及返回值,想要了解具体信息,可参考 cppreference.

参考资料

二分查找 红蓝染色法【基础算法精讲】 - 哔哩哔哩

二分查找有几种写法?它们的区别是什么? - 知乎

二分查找中的循环不变式_二分搜索的循环不变式 - groovy2007的博客

std::lower_bound - cppreference.com

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

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

相关文章

小型水库雨水情测报和大坝安全监测解决方案

一、建设背景 我国小型水库数量众多&#xff0c;大多由农村集体经济组织管理&#xff0c;灌溉、供水、防洪、生 态效益突出&#xff0c;是农业生产、农民生活、农村发展和区域防洪的重要基础设施&#xff0c;实施乡 村振兴战略和生态文明建设的重要支撑保障。由于小型水库工程存…

zabbix自定义监控内容案例

一、自定义监控内容 案列&#xff1a;自定义监控客户端服务器登录的人数需求&#xff1a;限制登录人数不超过 3 个&#xff0c;超过 3 个就发出报警信息 1、在客户端创建自定义key 明确需要执行的linux命令 创建zabbix监控项配置文件&#xff0c;用于自定义Key #在zabbix的…

小谈设计模式(3)—策略模式

小谈设计模式&#xff08;3&#xff09;—策略模式 专栏介绍专栏地址专栏介绍 策略模式主要角色环境&#xff08;Context&#xff09;抽象策略&#xff08;Strategy&#xff09;具体策略&#xff08;Concrete Strategy&#xff09;角色总结 核心思想封装算法定义抽象策略使用环…

Selenium Grid 的搭建方法

传统 Selenium Grid 的搭建方法 搭建一个具有 1 个 Node 的 Selenium Grid。那么通常来讲我们需要 2 台机器&#xff0c;其中一台作为 Hub&#xff0c;另外一台作为 Node&#xff0c;并要求这两台机器已经具备了 Java 执行环境。 1.通过官网下载 selenium-server-standalone-…

SpringMVC之JSON数据返回异常处理机制

目录 前言 一、JSON数据返回 1.导入依赖 2.配置spring-mvc.xml 3.使用ResponseBody注解 4.Jackson 4.1.介绍 4.2.常用注解 二、异常处理机制 1.为什么要全局异常处理 2.异常处理思路 3.SpringMVC异常分类 4.综合案例 4.1.异常处理方式一 4.2.异常处理方式二 4.3…

git提示:remote origin already exists

目录 问题场景 问题原因 问题解决 问题场景 在GitLab中新建仓库后&#xff0c;然后将本地项目提交提示&#xff1a;remote origin already exists. 问题原因 error: remote origin already exists. 错误&#xff1a;远程源点已存在&#xff08;翻译&#xff09; 出现该错误的…

AI AIgents时代-(四.)应用上手

HuggingGPT & MetaGPT . &#x1f7e2; HuggingGPT HuggingGPT是一个多模型调用的 Agent 框架&#xff0c;利用 ChatGPT 作为任务规划器&#xff0c;根据每个模型的描述来选择 HuggingFace 平台上可用的模型&#xff0c;最后根据模型的执行结果生成总结性的响应。 这个项…

软件测试 —— 答疑篇

什么是软件测试&#xff1a; 软件测试是不是就是找 bug &#xff1f; 软件测试就是证明软件不存在错误的过程 软件测试就是为了证明程序能够正确运行 刚新买来一部手机&#xff0c;我们要干什么&#xff1f; 一场考试 , 做完一遍题目之后 , 进行一遍检查 , 就是在 "…

【LeetCode热题100】--560.和为K的子数组

560.和为K的子数组 示例2的结果&#xff1a; 输入&#xff1a;nums [1,2,3] ,k3的时候 连续子数组有[1,2],[3]&#xff0c;一共有2个 利用枚举法&#xff1a; 枚举[0,…i]里所有的下标j来判断是否符合条件 class Solution {public int subarraySum(int[] nums, int k) {i…

不得不爱的AI艺术写真头像二维码生成小程序开发

最近什么最火&#xff1f;AI最火&#xff01; AI里什么最火&#xff1f;艺术写真生成和二维码美化最火。 一款小程序集合了高还原度的AI写真艺术照和二维码美化&#xff0c;你们说香还是不香&#xff1f; 并且加入了输入心愿就能生成独一无二的个性头像功能&#xff0c;直接…

(c/c++)——文件操作的知识补充

文章目录 一、文件的位置指针二、缓存区三、流状态一、文件的位置指针 在写入模型中,只有刷新,覆盖和追加的方式。若要在某地方进行插入,就需要位置指针 1)获取文件位置指针:ofstream类的成员函数是tellp();ifstream类的成员函数是tellg();fstream类两个都有,效果相同…

Redis command timed out 处理(InsCode AI 创作助手)

问题详情&#xff1a;redis命令超时异常 Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException: Command timed out after 3 second(s) 导致Redis命令超时的可能原因 Redis服务器负载高&#xff1a;Redis服务器负载过高可能导致命令…

人机合作的有效性、安全性和可信度

确定人机协同中权力归属的原则和方法可以根据具体情境和任务的要求进行灵活选择。以下是一些常见的方法&#xff1a; 专业领域授权&#xff1a;在专业领域中&#xff0c;权力可能更多地授予具有相关知识和经验的人类专家。他们能够理解和分析复杂的情况&#xff0c;并基于其专业…

uniapp 轮播列表左右滑动,滑动到中间放大

html <!-- 轮播 --><view class"heade"><swiper class"swiper" display-multiple-items3 circulartrue previous-margin1rpxnext-margin1rpx current0 change"swiperChange" ><block v-for"(item,index) in list"…

【面试经典150 | 数组】删除有序数组中的重复项 II

文章目录 写在前面Tag题目解读题目来源解题思路方法一&#xff1a;原地操作 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主&#xff0c;并附带一些对于本题涉及到的数据结构等…

淘宝拍立淘插件转链和商业化图片生成接口介绍,图片搜索商品接口,按图搜索接口,图片识别商品接口介绍

淘宝拍立淘是淘宝网推出的一种搜索方式&#xff0c;通过拍立淘&#xff0c;用户可以输入文字描述或上传图片来搜索商品。拍立淘通过与淘宝网进行数据接入和授权&#xff0c;使用淘宝提供的API获取商品信息和操作权限&#xff0c;拍立淘使用图像识别技术&#xff0c;通过深度学习…

在gazebo仿真环境中加载多个机器人

文章目录 前言一、基本概念1、xacro2、Gazebo 加载单个机器人模型 二、原先launch文件代码三、 修改launch文件加载多个机器人总结 前言 单个机器人的各项仿真实验都基本完成&#xff0c;也实现了远程控制&#xff0c;接下来主要对多机器人编队进行仿真实验&#xff0c;在进行…

2023年CCF-CSP考前冲刺

202305-1重复局面 思路&#xff1a; 题目的意思是我们输入n组局面&#xff0c;每个局面由64个字符组成&#xff0c;然后判断有没有相同局面。那么我们就可以开一个map&#xff0c;用字符数组a记录每个局面的字符&#xff0c;然后放入map中&#xff0c;每次输出它的次数即可。 …

9月19日,每日信息差

今天是2023年09月19日&#xff0c;以下是为您准备的21条信息差 第一、我国城市新能源公共汽电车占比达到77%&#xff0c;目前&#xff0c;全国共有城市公共汽电车70.3万辆&#xff0c;新能源公共汽电车超过54万辆&#xff0c;占比达到77%&#xff1b;共有54个城市开通轨道交通…

Flutter与Native通信原理剖析与实践

通信原理 我们分几种场景来介绍Flutter和Native之间的通信。 Native发送数据给FlutterFlutter发送数据给NativeFlutter发送数据给Native&#xff0c;然后Native回传数据给Flutter Flutter与Native通信机制 在讲解Flutter与Native之间是如何传递数据之前&#xff0c;我们先了…