C++算法初级7——二分查找

news/2024/5/21 22:09:30/文章来源:https://blog.csdn.net/bj_zhb/article/details/130043684

C++算法初级7——二分查找

文章目录

  • C++算法初级7——二分查找
      • 在升序的数组上进行二分查找
      • 总结
      • 应用范围
      • 应用

二分查找的原理:每次排除掉一半答案,使可能的答案区间快速缩小。

二分查找的时间复杂度:O(log n),因为每次询问会使可行区间的长度变为原来的一半。

我们再来看一下二分查找的思路:我们设定一个初始的L和R,保证答案在[L,R]中,当[L,R]中不止有一个数字的时候,取区间的中点M,询问这个中点和答案的关系,来判断答案是M,还是位于[L,M-1]中,还是位于[M+1,R]中。二分查找的伪代码如下:

int L = 区间左端点;
int R = 区间右端点; // 闭区间
while( L < R ) { // 区间内有至少两个数字int M = L+(R-L)/2; // 区间中点if( M是答案 ) 答对啦;else if( M比答案小 ) L = M+1;else R = M-1; // M比答案大
}
// 若运行到这里,因为答案一定存在,所以一定有L==R,且L是答案

在升序的数组上进行二分查找

在一个排好序的数组上二分查找一个数字x,一般都可以变成如下的问题:在数组中找到第一个大于等于x的数字的位置(假设数组是从小到大排好序的)。

问题: 输入n,x,以及一个长度为n的数组a(已经从小到大排好序了)

输出数组a中最左边的大于等于x的数字的下标,数组下标从0开始

输入数字都是1000000000以内的非负整数。数组长度不超过50000。若数组中不存在大于等于x的数字,输出-1
在这里插入图片描述
一些特殊情况:
请手动模拟一下这段代码在下面数组上的运行过程,体会一下这段代码是如何处理一些边界情况的。
在这里插入图片描述
比如:答案不存在的情况我们是如何处理的?

比如:当区间内只有两个数字的时候,这段代码还能正常运行吗?

比如:数组中有很多个重复元素的时候,这段代码还能正常运行吗?

比如:为什么循环结束之后一定有L == R?为什么不会出现L > R的情况?

请自己写一些简单情况出来,并手动模拟运行这段代码,想一想为什么这段代码不会出错。

比如:在2 3这个数组中找到第一个大于等于3的元素。

比如:在2 3 3 3 3 4 4 4 4这个数组中找到第一个大于等于4的元素。

倒过来怎么做
那现在,如果你面临一个新的问题:

有一个从小到大排好序的数组,你要找到从右向左数第一个小于等于x的数字,应该怎么做?
问题:输入n,x,以及一个长度为n的数组a(已经从小到大排好序了)

输入样例:

9 4

2 3 3 3 3 4 4 4 4

我们可以把问题转化为“找到从左往右数最后一个小于等于x的数字”,这时候就可以写出L = 0, R = n-1这样的初始条件。

有些复杂的问题,进行问题转换也是较为困难的,因此我们需要总结出一个不费脑子、不需要思考就可以写出优美代码的做法。

我们注意到,二分查找的精髓在于,只通过a[M]的值来判断:答案是在左半边还是在右半边。

因此,我们只要抛弃传统意义上的“大小”概念,牢牢抓住这一点进行分析,仔细推断出这个条件用到的表达式,就一定可以写出优美的代码。
在这里插入图片描述
糟糕!死循环!
但是!假设现在有L = 3, R = 4,你要找的是最后一个小于等于x = 100的数字,并且数组元素是a[3] = 80, a[4] = 90。

然后通过计算得到中点M = 3,检查发现a[3] <= 100,所以执行L = M,把答案的可行区间变成[M,R]。

你已经发现问题了!在经过一次二分之后,变量仍然保持了L = 3, R = 4没有变化,循环条件L < R一直被满足,我们始终无法结束循环。

这就让我们的程序进入了死循环!

为什么会死循环?

如果你实现了刚刚问题(有一个从小到大排好序的数组,你需要找到从左往右数最后一个小于等于x的数字)的代码,你可能会写出下面这样的代码。

int L = 0, R = n-1;
while( L < R ) {int M = L + (R - L)/2;if( a[M] <= x ) { // 答案一定在[M,R]中L = M;} else { // 答案一定在[L,M - 1]中R = M - 1;}
}
// a[L]就是答案

但是你发现,这个程序好像存在一些问题:有时候,程序会陷入死循环,无法得到运行结果。这是为什么呢?

和最初的问题对比一下,你能发现这两份代码的不同之处吗:

// 最初的问题:在数组中找到从左往右 第一个 大于等于x的数字的位置
if( 答案在[M + 1,R]) {L = M + 1;
} else {R = M; // 这里可能引发“差一点”问题
}
// 现在的问题:在数组中找到从左往右数 最后一个 小于等于x的数字
if( 答案在[M, R]) {L = M;
} else {R = M - 1
}

这段代码在逻辑上肯定是没有错误的 —— 你每次都把正确的区间挑选出来了。那为什么这段代码会在某些时候引起死循环呢?

如何避免问题?
事实上,死循环只会在刚刚这种况出现:

假设现在有L = 3, R = 4,你要找的是最后一个小于等于x = 100的数字,并且数组元素是a[3] = 80, a[4] = 90。

然后通过计算得到中点M = 3,检查发现a[3] <= 100,所以执行L = M,把答案的可行区间变成[M, R]。

在经过一次二分之后,变量仍然保持了L = 3, R = 4没有变化,循环条件L < R一直被满足,我们始终无法结束循环。

这是因为我们在判断出答案在[M, R]中的时候,执行了L = M这句话,而根据我们的中点计算公式M = L + (R - L)/2,我们在R == L+1的情况下总会得到L == M。所以我们在经过一次二分之后,L和R的值没有发生变化,也就陷入了死循环。

要避免这个问题,其实也非常简单,我们只需要把中点计算公式变成M = L + (R - L + 1)/2即可。在之前的中点计算公式M = L + (R - L)/2中,我们如果遇到了中点不是整数的情况,则会把中点向下取整,因此在出现L + 1 == R这种情况的时候就会始终有L == M从而引发问题。现在我们通过一个+1使得在中点不是整数的时候把中点向上取整,就可以避免这个问题(请在纸上模拟代码的运行过程,以体会这个公式是如何解决“差一点”问题的)。

总结

在这里插入图片描述

正如之前说的,二分查找中其实还有很多细节问题没有处理,比如:

  • 如果循环最后因为不满足L < R条件而退出,这时候L和R到底是什么关系?答案是什么?
  • 如果答案不存在会怎么样?

应用范围

如果我们想要在一个数组上进行二分查找,那么这个数组必须是有序的,不管是升序还是降序,它必须是有序的。为什么呢?

注意二分查找的本质是什么:通过比较数组中间那个值和我们要求的值的关系,来判断出“答案不可能出现在数组的某一半”,从而让我们的查找范围缩小为原来的一半。
在这里插入图片描述
这也就是为什么我们要求数组中的元素是满足单调性的:只有这样,我们才能保证当a[M]不满足条件的时候,它左边(或者右边)的所有元素都不满足条件。

那么是不是任何有序的数据结构都可以应用二分查找算法呢?

日期

日期是一个天然有序的结构:我们可以定义日期A小于日期B意为:在日历上A排在B的前面。比较两个日期的大小也可以通过很简单的方式进行:先比较年,再比较月,最后比较日。

struct Date {int year, month, day;
};
bool operator<( const Date &a, const Date &b ) {if( a.year == b.year ) {if( a.month == b.month ) {return a.day < b.day;} else {return a.month < b.month;}} else {return a.year < b.year;}
}

但是我们可能会面临一个问题:如果我们要在公元1年1月1日和1000000000年1月1日之间二分,我们该如何求出两个日期的中点呢?

我们把日期表示成YYYYMMDD的形式,比如公元1年1月1日就是00010101,1000000000年1月1日就是10000000000101。则两个日期的中点,就是两个数字的中点,只不过我们需要把这个数字向下取整(或者向上取整)到最近的合法的日期。

比如,我们要求19701212和20200817的中点,我们可以直接求(19701212 + 20200827) / 2 = 19951019,这就是这两个日期的近似中点。如果我们得到了类似于19971805这样不合法的日期(没有18月),我们只需要把18月向下取整到合法的日期(12月),变为19971205即可。

字符串

字符串也是一个天然有序的数据结构:字典序就是字符串的大小顺序。因此我们可以给一堆字符串按照字典序排序。

string s[100];
for( int i = 0; i < n; ++i )cin >> s[i];
// sort函数用于给数组中的元素排序
sort(s, s+n); // string类的比较函数为比较两个字符串的字典序

现在在一堆排好序的字符串中,我们要找出所有前缀是com的字符串,应该怎么做呢?
容易发现,所有前缀是com的字符串,在数组中也是一个连续的区间。

我们可以把数组中的所有字符串截断到前3位,然后使用二分查找法找到第一个com出现的位置和最后一个com出现的位置。

在这之间的所有字符串,前缀都是com。

有的时候我们需要用到二维数据,比如平面中的点,就需要两个数字来表示,再比如std::pair这个数据结构,就是简单地把两个数字组合在一起。

不妨假设我们遇到的二维数据都是下面这样子的。类似平面上的整数点,一个点用两个整数(x,y)表示。

struct Point {int x, y;
};
// 这是运算符重载,当我们在代码中用小于号比较两个Point类变量的时候,就会用这个函数进行比较
bool operator<( const Point &a, const Point &b ) { // 如何定义a < bif( a.x == b.x ) {return a.y < b.y;} else {return a.x < b.x;}
}

这里我们定义了一种常用的比较二维数据的方法:首先比较两个数据的第一维,数字小的排在前面,当第一维数字相同的时候,比较第二维,数字小的排在前面。比如(3,3) < (4,2),因为先比较第一维3 < 4。再比如(2,3) < (2,5),因为第一维相同时比较第二维。

如果我们有一个排好序的Point数组,我们想找到数组中所有x = 5的元素(容易发现所有x = 5的元素在数组中一定是一个连续的区间),应该怎么做呢?

一个排好序的Point数组例子:(1,2), (2,3), (2,4), (5,-1), (5,2), (5,5), (7,4)。

Point a[100000];
for( int i = 0; i < n; ++i )
cin >> a[i].x >> a[i].y;
sort(a, a+n); // sort函数可以给数组中的元素排序

我们只需要两次二分查找就可以了:分别找到第一个大于等于Point(5, INT_MIN)的元素,以及最后一个小于等于Point(5, INT_MAX)的元素。这两个元素中间的所有元素就是x = 5的所有元素(闭区间)。INT_MIN和INT_MAX分别是int所能表达的最小值和最大值。

应用

我们总结一些二分查找的常见应用:

  1. lower_bound和upper_bound

lower_bound的用途是:在指定的升序排序的数组中,找到第一个大于等于x的数字。

upper_bound的用途是:在指定的升序排序的数组中,找到第一个大于x的数字。

使用lower_bound和upper_bound可以帮我们解决绝大多数二分查找问题。

这两个函数会返回对应数字的指针。示例代码如下:

int a[100000], n;
cin >> n;
for( int i = 0; i < n; ++i )cin >> a[i];
sort(a, a + n);
int *p = lower_bound(a, a + n, 13); // 第一个大于等于13的数字
int *q = upper_bound(a, a + n, 13); // 第一个大于13的数字

假如我们使用lower_bound和upper_bound二分查找同一个数字13,容易发现,我们得到的两个指针构成了一个左闭右开区间,这个区间里全部都是数字13。

巧妙地运用这两个函数,可以完成所有常见的二分查找操作:

  • 找到第一个大于等于x的数字
  • 找到第一个大于x的数字
  • 找到最后一个等于x的数字
  • 查找数组中是否有数字x
  • 查询数组中有几个数字x
  • 找到最后一个小于x的数字
  1. 二分法可以求方程的近似解。

  2. 二分法可以用来优美地实现离散化操作。

  3. 在double上二分时,尽量使用固定次数二分的方法。

求方程的解
问题

请输出方程x^3 + 16 = 0的解,已知这个解在[-1e9,1e9]之间,并且函数f(x) = x^3 + 16在定义域上单调递增。输出的答案保留5位小数。

我们现在想求出某个方程f(x) = 0的解,并且我们知道这个解在[L,R]之间,且函数f(x)在[L,R]上单调递增。我们只需要这个解精确到5位小数即可。
在这里插入图片描述
在这里插入图片描述
在double上二分的注意事项
在这里插入图片描述

你可能会发现,最终的结果并没有精确到10位小数,或者是这个二分直接陷入了死循环。

在精度要求越高的时候,就越可能出现这样匪夷所思的情况。

这是因为double本身存在不小的精度误差,我们通过R - L >= 1e-10这种方式来控制二分的终止条件,会带来非常大的精度问题。

这种时候,我们可以采用固定次数二分的方法:

double L = -1e9, R = 1e9;
for( int times = 0; times < 100; ++times ) { // 二分100次double mid = (L+R)/2;// 此处省略二分内容
}

这里我们二分100次,是因为2的100次方约为1e30,而我们二分的初始条件是1e9左右,足以在最后把精度控制在1e-20左右。

在这种二分策略下,我们一般都能得到合理的答案。

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

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

相关文章

appium+python自动化测试启动app

一、部署环境 1、依次下载安装以下工具&#xff0c;并配置环境变量&#xff1a; android sdk Nodejs appium appium-doctor Appium-Python-Client pycharm64 ps:安装包下载和配置环境变量的操作步骤跟着网上各路大神的帖子一步一步做就好了&#xff0c;没啥难度 二、连…

Machine Learning-Ex4(吴恩达课后习题)Neural Networks Learning

目录 1. Neural Networks 1.1 Visualizing the data 1.2 Model representation 1.3 Feedforward and cost function 1.4 Regularized cost function 2. Backpropagation 2.1 Sigmoid gradient 2.2 Random initialization 2.3 Backpropagation 2.4 Gradient Checking…

【数据库原理 • 四】数据库设计和规范化理论

前言 数据库技术是计算机科学技术中发展最快&#xff0c;应用最广的技术之一&#xff0c;它是专门研究如何科学的组织和存储数据&#xff0c;如何高效地获取和处理数据的技术。它已成为各行各业存储数据、管理信息、共享资源和决策支持的最先进&#xff0c;最常用的技术。 当前…

linux之jdk1.8环境安装与配置和Maven安装与配置

文章目录一、jdk1.8环境安装1、官网下载&#xff1a;<https://www.oracle.com/java/technologies/downloads/#java8>2、在usr文件夹下新建一个java文件夹3、解压完成后&#xff0c;将文件jdk文件传入到java目录下二、配置环境&#xff08;重点&#xff09;1、按 i 进行编…

蓝牙技术|苹果获空间音频新专利,AirPods可动态调整声学输出

美国商标和专利局&#xff08;USPTO&#xff09;公示的清单显示&#xff0c;苹果在近日获得了一项名为“测定虚拟聆听环境”的新专利。据悉&#xff0c;该技术可以改善用户的聆听体验&#xff0c;增强空间音频的沉浸感&#xff0c;未来有望应用在AirPods上。 这项专利技术可以…

代码随想录_二叉树_二叉树的层序遍历十道题

leetcode102.二叉树的程序遍历 102. 二叉树的层序遍历 给你二叉树的根节点 root &#xff0c;返回其节点值的 层序遍历 。 &#xff08;即逐层地&#xff0c;从左到右访问所有节点&#xff09;。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&…

文心一言对于宣传文案理解

前言 前段时间对于文心一言开放部分内测邀请&#xff0c;有幸获得邀请内测权限&#xff01;抱着试一试的态度对其进行了使用&#xff0c;结果还是比较满意的。我们来看一下我所说的满意是否能够达到你的要求&#xff01;&#xff01;&#xff01; 使用逻辑 文心一言的使用还…

FPGA lattice 深力科LCMXO3LF-4300C-6BG256I 可实现高效、灵活和安全的工业应用开发 低功耗FPGA解决方案详情讲解

FPGA lattice 深力科LCMXO3LF-4300C-6BG256I 可实现高效、灵活和安全的工业应用开发 低功耗FPGA解决方案详情讲解 超低密度FPGA 是最新的立即启用、非挥发性、小型覆盖区 FPGA&#xff0c;采用先进的封装技术&#xff0c;能让每个元件达到最低成本。此系列采用最新的小型封装&…

android12 displayArea学习

一&#xff1a;数据结构分析 1&#xff1a;android 12 WindowContainer 的类继承关系如下 下图为 WindowContainer 简要的对象图。 下图是 Aosp默认的display层次结构对象图。 Aosp定义的feature有如下 FEATURE_ROOT 0; FEATURE_DEFAULT_TASK_CONTAINER 1; FEATURE_WINDOW_…

正版软件 Directory Opus 12 Pro Windows 平台上的资源管理器,定是功能完全、可定制化程度高的那款。

Directory Opus 是一款 Windows 平台上的资源管理器&#xff0c;定是功能最完全、可定制化程度最高的那款。你可以通过它完成几乎所有操作&#xff0c;包括查看图片元信息、预览图片、阅读文本文件内容、批量重命名、操作压缩文件以及 FTP 同步请求等。 Directory Opus 是一款由…

使用大华惠智双目半球网络摄像机DH-IPC-HD4140X-E2获取人流量统计数据

记录一下使用Java的SpringBoot大华SDK在智慧公厕项目中使大华惠智双目半球网络摄像机DH-IPC-HD4140X-E2获取人流量统计数据 首先根据说明书登录摄像头&#xff0c;一般摄像头都有自己的账号和密码(可能是admin admin 也可能是admin 888888 还有可能是admin 12345)&#xff0c;…

DriveGPT、车企订单背后,为什么毫末每年都能搞出新东西?

作者 | 祥威 编辑 | 德新 4月11日&#xff0c;毫末智行正式发布自动驾驶生成式大模型 DriveGPT&#xff0c;中文名 雪湖海若&#xff0c;可以提升自动驾驶认知能力&#xff0c;最终提升规控效率。 雪湖海若的核心&#xff0c;是将各种驾驶场景作为Token输入到模型中&#xff…

零基础搭建私人影音媒体平台【远程访问Jellyfin播放器】

文章目录1. 前言2. Jellyfin服务网站搭建2.1. Jellyfin下载和安装2.2. Jellyfin网页测试3.本地网页发布3.1 cpolar的安装和注册3.2 Cpolar云端设置3.3 Cpolar本地设置4.公网访问测试5. 结语1. 前言 随着移动智能设备的普及&#xff0c;各种各样的使用需求也被开发出来&#xf…

react 1:jsx-组件-状态-事件

主要是为了解决什么问题 &#xff1f; 构建那些数据会随时间改变的大型应用 React 特点 虚拟 DOM 组件系统 单向数据流 jsx语法 jsx语法 组件创建方式 两种&#xff1a;class类组件&#xff0c;函数方式创建 代码快捷生成插件&#xff1a;&#xff1a;&#xff1a;rc…

算法 || DFS(深度优先搜索) BFS(广度优先搜索)

&#xff11;、基本概念 dfs全称为Depth First Search,即深度优先搜索。它的思想是沿着每一条可能的路径一个节点一个节点地往下搜索,搜到了路径的到终点再回溯,一直到所有路径搜索完为止。 bfsbfs全称为Breath First Search,即广度(宽度)优先搜索。它的思想是将每一层的结搜…

浅谈 如果做微服务了 这个模块怎么去划分?

如果做微服务了 这个模块怎么去划分&#xff1f; 还是高内聚 低耦合的一个思想吧 &#xff0c;单一职责的设计原则&#xff0c;也是一个封装的思想吧&#xff0c; 业务维度&#xff1a; ​ 按照业务的关联程度来决定&#xff0c;关联比较密切的业务适合拆分为一个微服务&…

Pandas.read_excel详解

文章目录基础知识语法参数详解-index_col参数详解-header参数详解-usecols参数详解-dtype其他参数多表读取基础知识 pandas 可以读取多种的数据格式&#xff0c;针对excel来说&#xff0c;可以使用read_excel()读取数据&#xff0c;如下&#xff1a; import pandas as pd dfp…

linux下使用lftp的小结

lftp的功能比较强大&#xff0c;相比原来用ftp&#xff0c;方便了很多。 1、登陆&#xff1a; lftp ftp://yournamesite pwd:***** 或 open ftp://yournamesite 2、基本操作&#xff08;转&#xff09; lftp使用介绍 lftp 是一个功能强大的下载工具&#xff0c;它支持访问…

JS Hook 基本使用

前言 Hook技术也叫钩子函数&#xff0c;功能是把网站的代码拉出来&#xff0c;改成我们自己想执行的代码片段&#xff0c;简单来说就是可以控制执行函数的入参和出参&#xff1b; 一、资源下载 编程猫插件&#xff1a;https://pan.baidu.com/s/1SP8xHoDpugssFRpu-nLxPw?pwdz…