二分查找算法
我身边的人都认为二分查找很简单,但事实真是如此吗?不,并不简单。二分算法有着许多的边界问题,当你写着代码一不小心就会陷入死循环。本篇文章会深入细节详细介绍整数二分算法以及使用二分算法步骤和力扣题目练习,并且还会给出二分查找算法模板,下面就然我们来看看吧。
单调性想必大家都不陌生,给出一段有序区间,找到它的中间值mid,如果中间值小于目标值的话那么答案在右边,如果中间值大于目标值的话那么答案在左边。

那么边界又是个什么东西呢?我们给定一段区间,在这个区间上我们给定了一种性质,使得在这个区间的右半边是满足这个性质的,在这个区间的左半边是不满足这个性质的。那么此时我们就可以使用二分,来找出满足这个性质和不满足这个性质的边界点。

算法思路:假设答案在闭区间[l, r]中, 每次将区间长度缩小一半,当l = r时,我们就找到了答案。
接着我们来看看二分算法的主要思想。现在给我们一个区间,在这个区间上我们给定了一种性质,使得在这个区间的右半边是满足这个性质的,在这个区间的左半边是不满足这个性质的。假设我们现在要找出左区间也就是红色区间的边界点,图中用黄色点标出了,我们应该怎样做呢?
首先我们先确定中间值mid,然后我们写一个check函数,接着根据check函数更新答案所在区间。

此时mid是满足红色性质的,所以mid落在红色区间内,所以mid是有可能为答案的,这里的答案指的是我们要而分出的边界点也就是黄色点。所以此时答案所在区间就是[mid,R];那我们要更新答案区间,就要让L=mid。

此时mid是不满足红色性质的,所以mid没有落在红色区间内,此时mid不可能为答案,所以此时答案所在区间就是[L,mid-1];那我们要更新答案区间,就要让R=mid-1。
下面是二分查找算法的模板,这个模板几乎可以胜任所有的二分题,建议背过。
版本1
当我们将区间[l, r]划分成[l, mid]和[mid + 1, r]时,其更新操作是 r = mid,计算mid时不需要加1。
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = (l + r)/2;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
版本2
当我们将区间[l, r]划分成[l, mid - 1]和[mid, r]时,其更新操作是l = mid;,此时为了防止死循环,计算mid时需要加1。
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid =(l + r + 1)/2;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
二分模板一共有两个,分别适用于不同情况。但其本质的区别就是mid = ( l + r ) / 2时需不要加上1。注意:当更新操作为其更新操作是 r = mid,计算mid时不需要加1。其更新操作是l = mid;,此时为了防止死循环,计算mid时需要加1。
加上1的目的是为了防止死循环,这个我们会在后面的题目解释。
那么这个两个模板要怎么使用呢? 我们来做几道题目,从这几道题目中我来给你们解释。
–>题目链接

这道题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,就要考虑是不是可以用二分法了。
这里我们看了题目,发现可以使用二分法。那么我们就用刚刚的模板来看看吧,那要怎么用呢?
首先我们先确定中间值mid,现在有一区间[-1,12],我们定义左端点和右端点,接着我们计算出mid的值为(0+5)/2=2;
接着我们写一个check函数,可以看到target为9,那么此时区间是不是满足一个性质,这个性质把区间分为两部分,蓝色部分的值小于9,绿色区间的值大于等于9。 此时我们这个check函数可以定义为if(mid的值>=9),就是判断mid是不是满足大于等于9这个性质。
如果mid满足这个特性就说明它落在绿色区域,此时我们就要更新答案所在区间,此时答案在[L,mid]中,我们的更新操作为R=mid;由于更新操作是R=mid,所以这里的mid = ( l + r ) / 2时不需要加上1!!!,我们自然而然使用第一个模板。

但此时mid的下标为2所对应的值时3,它是不满足>=9这个性质的,所以mid此时落在蓝色区域,此时我们就要更新答案所在区间,此时答案在[mid,R]中,我们的更新操作为L=mid+1;
然后我们不断缩小答案所在范围,直到这个区间只有一个数时也就是L==R,那此时区间内唯一的那个数就是我们要找的答案,也就是这个性质的边界点。
看看代码实现
int l=0,r=nums.size()-1;
while(l < r)
{
int mid = (l + r) /2;
if(nums[mid] >= target) r = mid;
else l = mid + 1;
}
return l;

但是当我们提交答案后,发现这个测试样例出错了。
target为2,但是这个区间内并没有2,那我们此时二分查找出来的边界点是什么呢?
我们再回到刚刚的样例,我们知道二分查找的本质是处理边界问题,我们给这段区间一个性质,那么这个性质是一定有边界的,所以二分是一定有解的。这里我们找到的性质是target右边是大于等于9的,target左边是小于9的,这个性质把区间分为了两部分,绿色部分是满足这个性质的,蓝色部分是不满足这个性质的。那么我们这里的二分查找出来的这个值就是这个性质的边界点。

现在我们回到刚刚报错的测试样例,那如果这个区间里没有这个target呢?我们二分查找出来的这个值就是这个性质的边界点,此时我们的的性质是target右边是大于等于2的,target左边小于2的,也就是说我们会找到这个区间里从左往右第一个大于等于2的点。 这句话很重要,请重复理解。因此我们找到的值为3,它的下标为2,与预期结果-1不符,所以错误。
对此我们可以加一个判断条件,如果我们二分出来的值不等于target的话就return-1;
完整代码
int l=0,r=nums.size()-1;
while(l < r)
{
int mid = (l + r) /2;
if(nums[mid] >= target) r = mid;
else l = mid + 1;
}
if(nums[l] != target) return -1;
return l;
–题目链接

这道题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,就要考虑是不是可以用二分法了。 这里我们再次提起这句话,什么时候考虑使用二分。
解题过程
看看代码
int l=0,r=nums.size()-1;
while(l<r)
{
int mid=l+r>>1;
if(nums[mid]>=target) r=mid;
else l=mid+1;
}
return l;
但是当我们提交后,没错,又有错误!!

int l=0,r=nums.size()-1;
if(nums[r]<target)
return r+1;
while(l<r)
{
int mid=l+r>>1;
if(nums[mid]>=target) r=mid;//此时更新方式是r=mid,所以求mid不需要加上1
else l=mid+1;
}
return l;
题目链接

这道题目是什么意思呢?假设我们有一组数据,[5,7,7,8,8,10],此时target=8,那我们就返回数据中8的开始下标和结束下标。若数据内不存在target则返回[-1,-1]。

老规矩,看做题步骤。
解题过程
首先是寻找开始下标,就拿上面图片的样例来分析,此时这个区间是不是被一个性质分成了两部分,这个性质是什么呢,可以看到开始下标左边的数都是小于8的,开始下标右边的数都是大于等于8的,这个性质把区间分为了两个部分,我们用二分就可以找出这个性质的边界点,也就是这里的开始下标的位置。

那么具体代码是什么呢?
int l=0,r=nums.size()-1;
while(l<r)
{
int mid=(l+r)/2;
if(nums[mid]>=target) r=mid;
else l=mid+1;
}

接着是寻找结束下标,此时这个区间是不是被一个性质分成了两部分,这个性质是什么呢,可以看到结束下标左边的数都是大于等于8的,结束下标右边的数都是大于8的,这个性质把区间分为了两个部分,我们用二分就可以找出这个性质的边界点,也就是这里的结束下标的位置。

下面是具体代码
可以看到更新方式为L=mid;所以这里的mid = ( l + r ) / 2时需要加上1,这里我们就使用了第二个模板。
int l=0,r=nums.size()-1;
while(l<r)
{
int mid=(l+r+1)/2;
if(nums[mid]<=target) l=mid;
else r=mid-1;
}
下面是完整代码
vector<int> res;
//如果我们的数据为空,直接返回[-1,-1]
if(nums.size()==0)
{
res.push_back(-1);
res.push_back(-1);
return res;
}
//二分开始下标
int l=0,r=nums.size()-1;
while(l<r)
{
int mid=(l+r)/2;
if(nums[mid]>=target) r=mid;
else l=mid+1;
}
//当target值不存在时,return[-1,-1]
if(nums[l]!=target)
{
res.push_back(-1);
res.push_back(-1);
return res;
}
res.push_back(l);
//二分结束下标
l=0,r=nums.size()-1;
while(l<r)
{
int mid=(l+r+1)/2;
if(nums[mid]<=target) l=mid;
else r=mid-1;
}
res.push_back(l);
return res;
好了,三道题目做完了,不知道你还记得那两个模板吗,一个模板在求mid是需要加上一,一个不需要。但是你知道为什么吗?其实在前面我已经提到过了,是为了防止死循环所以我们加上了一个1,下面我们来分析下;
以上我们介绍的都是整数二分算法,整数二分算法需要考虑许多边界问题,因此细节比较多,但浮点数二分简单多了。这里我们就去介绍了,同样的我会给出浮点数二分算法的模板。
double bsearch_3(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
这里有两道题目,大家可以自己去尝试做一做。
69.x 的平方根
367.有效的完全平方数
记住我们的做题步骤,然后套用上面的模板。
解题过程
然后我们分析了整数模板为什么需要加上1,其原因是为了防止死循环。
好了,这篇文章就分享到这,如果对你有帮助的话,请点赞关注,支持一下吧!
我刚刚被困在这个问题上一段时间了。以这个基地为例:moduleTopclassTestendmoduleFooendend稍后,我可以通过这样做在Foo中定义扩展Test的类:moduleTopmoduleFooclassSomeTest但是,如果我尝试通过使用::指定模块来最小化缩进:moduleTop::FooclassFailure这失败了:NameError:uninitializedconstantTop::Foo::Test这是一个错误,还是仅仅是Ruby解析变量名的方式的逻辑结果? 最佳答案 Isthisabug,or
我正在尝试解析一个CSV文件并使用SQL命令自动为其创建一个表。CSV中的第一行给出了列标题。但我需要推断每个列的类型。Ruby中是否有任何函数可以找到每个字段中内容的类型。例如,CSV行:"12012","Test","1233.22","12:21:22","10/10/2009"应该产生像这样的类型['integer','string','float','time','date']谢谢! 最佳答案 require'time'defto_something(str)if(num=Integer(str)rescueFloat(s
目录一.加解密算法数字签名对称加密DES(DataEncryptionStandard)3DES(TripleDES)AES(AdvancedEncryptionStandard)RSA加密法DSA(DigitalSignatureAlgorithm)ECC(EllipticCurvesCryptography)非对称加密签名与加密过程非对称加密的应用对称加密与非对称加密的结合二.数字证书图解一.加解密算法加密简单而言就是通过一种算法将明文信息转换成密文信息,信息的的接收方能够通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,算法可以分为对称加密、非对称加密、对称加密和非
我有一个应用需要发送用户事件邀请。当用户邀请friend(用户)参加事件时,如果尚不存在将用户连接到该事件的新记录,则会创建该记录。我的模型由用户、事件和events_user组成。classEventdefinvite(user_id,*args)user_id.eachdo|u|e=EventsUser.find_or_create_by_event_id_and_user_id(self.id,u)e.save!endendend用法Event.first.invite([1,2,3])我不认为以上是完成我的任务的最有效方法。我设想了一种方法,例如Model.find_or_cr
我想找到给定字符串中的所有匹配项,包括重叠匹配项。我怎样才能实现它?#Example"a-b-c-d".???(/\w-\w/)#=>["a-b","b-c","c-d"]expected#Solutionwithoutoverlappedresults"a-b-c-d".scan(/\w-\w/)#=>["a-b","c-d"],but"b-c"ismissing 最佳答案 在积极的前瞻中使用捕获:"a-b-c-d".scan(/(?=(\w-\w))/).flatten#=>["a-b","b-c","c-d"]参见Rubyde
我的问题很简单:我是否必须在使用RubyonRails的类上require'csv'?如果我打开一个railsconsole并尝试使用CSVgem它可以工作,但我必须在文件中这样做吗? 最佳答案 CSVlibrary是ruby标准库的一部分;它不是gem(即第三方库)。与所有标准库(与核心库不同)一样,csv不会由ruby解释器自动加载。所以是的,在您的应用程序中某处您确实需要要求它:irb(main):001:0>CSVNameError:uninitializedconstantCSVfrom(irb):1from/Us
这应该是一个简单的问题,但我找不到任何相关信息。给定一个Ruby中的正则表达式,对于每个匹配项,我需要检索匹配的模式$1、$2,但我还需要匹配位置。我知道=~运算符为我提供了第一个匹配项的位置,而string.scan(/regex/)为我提供了所有匹配模式。如果可能,我需要在同一步骤中获得两个结果。 最佳答案 MatchDatastring.scan(regex)do$1#Patternatfirstposition$2#Patternatsecondposition$~.offset(1)#Startingandendingpo
我有以下数组:arr=[1,3,2,5,2,4,2,2,4,4,2,2,4,2,1,5]我想要一个包含前三个奇数元素的数组。我知道我可以做到:arr.select(&:odd?).take(3)但我想避免遍历整个数组,而是在找到第三个匹配项后返回。我想出了以下解决方案,我相信它可以满足我的要求:my_arr.each_with_object([])do|el,memo|memo但是有没有更简单/惯用的方法来做到这一点? 最佳答案 使用lazyenumerator与Enumerable#lazy:arr.lazy.select(&:o
假设您有一个可执行文件foo.rb,其库bar.rb的布局如下:/bin/foo.rb/lib/bar.rb在foo.rb的header中放置以下要求以在bar.rb中引入功能:requireFile.dirname(__FILE__)+"../lib/bar.rb"只要对foo.rb的所有调用都是直接的,这就可以正常工作。如果你把$HOME/project和符号链接(symboliclink)foo.rb放入$HOME/usr/bin,然后__FILE__解析为$HOME/usr/bin/foo.rb,因此无法找到bar.rb关于foo.rb的目录名.我意识到像rubygems这
是否有内置的Ruby方法或众所周知的库可以返回对象的整个方法查找链?Ruby查看一系列令人困惑的类(如thisquestion中所讨论)以查找与消息对应的实例方法,如果没有类响应消息,则调用接收方的method_missing。我将以下代码放在一起,但我确信它遗漏了某些情况或者它是否100%正确。请指出任何缺陷并指导我找到一些更好的代码(如果存在)。defmethod_lookup_chain(obj,result=[obj.singleton_class])ifobj.instance_of?Classreturnadd_modules(result)ifresult.last==B