【ZZ】二分查找七种情况大合集

二分查找七种情况大合集,核心不变

  二分查找,其实是非常简单的算法,却令自己非常容易晕头转向。早该抽取时间好好推一推,怪自己太懒。今年市赛有题卡了2小时,就是因为二分写不出。写出来样例都对,却无限WA,找不到问题,还影响心态。前一段时间好好梳理了一下,在这里贴出来,警示自己。。/(ㄒoㄒ)/。

  在此之前我是借鉴了各路大佬的博客来验证自己的想法的,贴一下借鉴的大佬博客:http://poj.org/problem?id=2299

  二分已经很多人写了,但自己梳理一下,饱含啰嗦的废话,希望大家都能更好简单的理清楚二分的简单,以俺自己的理解方式整理记录,也给自己存个纪念意义吧。

 

先讲一些废话:

废话一~:

  网上的二分杂七杂八,总感觉一样的命题,为什么写法总感觉不同,看着心痛。有一个原因是因为二分的区间使用不同:有人是使用开区间如(0,n),有人是用闭区间[0,n],有人是用左闭区间有开区间[0,n),等等。

故在这里先声明,我使用的区间都是闭区间:左闭右闭——[L,R]例如[0,n],而且我们假定序列是从小到大排序的!(递增),就是左边小右边大!

  为了方便代码的书写,区间包括了0也包括了n,也就是说其实一共有n+1个元素。其实下标从1开始也无碍的,在这里取0作为其实下标好了。在二分的过程中,使用[L,R]表示使用二分的数组区间,而使用小写的字母l表示二分当前进行到的区间的左边界,r表示二分当前进行到的区间右边界。

废话二~:

  还有一个是为了程序的更好的鲁棒性:求中间值的时候,我们多用m=(l+r)/2; 但是这样的可能会越界,比如 l+r超int,所以我们用更好更安全的写法 :m=l+(r-l)/2;   起初还钻牛角尖不理解为什么等式相同。。。

废话三~:

  看大牛的代码多用“按位移”(>>1)来代替除法的,那要考虑优先级。个人印象是>>优先级很低,不但比乘法低,还比加法低,所以要加括括号饿。本菜还是乖巧的使用/好了,更清晰明了。

废话四~:

  很常见的,二分查找的递推写法都是while(l<=r){....},那么循环结束之时,肯定是 l!=r的结果,此时必然是 l=r+1(l跑到了r的右边),并且是 r=l-1(r跑到了l的左边)。所以后面的代码在return部分,使用r+1和使用l进行return是等价的一样的,或者使用r和使用l-1进行return是等价的一样的,不需要感到疑惑。

 

进入正题:

  先给出结论:二分查找其实就是插一个FLAG(擂台),然后进行不断二分比对的过程中,如果没达到FLAG标准,就继续向自己心中的方向走(这个方向是相对的);如果达到FLAG标准了,立马毫不犹豫犯怂回退。

  所以二分查找的核心就是如何插FLAG,以及如何确定自己心中的方向。

   所谓“FLAG”,就是本二分的查找目的是什么。而“方向”,就是我们在执行二分查找,而不断缩小区间的行为中,向着目的正方向的行为,就是收敛二分查找的方向,收敛方向。

  在后头我将结合7种情况,分别进行阐释这段用饱含着中二、打怪的不正常思路写下的这句总结的话。

 

首先,我们可以确定一下,二分变来变去常用的有这么七种,我建议朋友们可以先看①(因为这是最基础的写法呀!),然后再先看⑥和⑦,看懂了之后再返回前面去看,不仅是因为这两个和第一个题目有点像,而且我觉得理解的时候最容易理解,我写的也多一些~:

  ①是否 存在数字t                                  ——返回下标或者-1

  ②找到 大于t的第一个数                        ——返回下标或者-1

  ③找到 大于等于t的第一个数                 ——返回下标或者-1

  ④找到 小于t的最后一个数        ——返回下标或者-1

  ⑤找到 小于等于 t的最后一个数       ——返回下标或者-1

  ⑥是否 存在数字t,返回 第一个t        ——返回下标或者-1

  ⑦是否 存在数字t,返回 最后一个t     ——返回下标或者-1

 

 

  可以发现,情况①和情况⑥和情况⑦是很像但是却有区别的。第①种情况是最最简单的二分,默认返回的是中间位置的t,就是“赤裸的二分”。第②、⑤、⑦的代码是相互相像的,而第③、④、⑥的代码是相互相像的。

  下面我会直接先贴出代码的核心写法(因为其他部分都一毛一样的),然后对应讲解自己的“FLAG”思维。在本文的最后,俺会一次性的贴出完整的函数代码。

  数组假设已经从小到大排序完毕。

 

   ①是否存在数字t:(赤裸的二分查找,最原始)

 

while(l<=j)
{
m=l+(r-l)/2;
if(a[m]==t)return m; //达成目标
else if(a[m]<t)l=m+1; //比t小,向前走
else if(a[m]>t)r=m-1; //比t大,向后退
}
return -1;
 这是最简单原始的二分,没有设立FLAG。

 

 ②找到 大于t的第一个数:

//第一个大于,应当向右收敛
while(l<=r)
{
m=l+(r-l)/2;
if(a[m]<=t) l=m+1; //未达到FLAG,区间向右收敛
else if(a[m]>t)r=m-1; //触发FLAG,怂,退回来
}
return l>R?-1:l; //假设所有的数字都小于等于t,也就是永远触发不了FLAG,
//l就会不断右移,出现上溢
 设立的FLAG:大于t的第一个数。遥想一下,给一个数组,我们肉眼去扫寻的话,眼球肯定是从左边往右边扫最快,所以——方向向右

 

 ③找到 大于等于t的第一个数:

//第一个大于等于,应当向右收敛
int l=L,r=R,m;
while(l<=r)
{
m=l+(r-l)/2;
if(a[m]<t)l=m+1; //未达到FLAG,区间向右收敛
else if(a[m]>=t)r=m-1;//触发FLAG,怂,退回来
}
return l>R?-1:l; //假设所有的数字都小于t,也就是永远触发不了FLAG,
//l就会不断右移,出现上溢
 设立的FLAG:大于等于t的第一个数,遥想一下,给一个数组,我们肉眼去扫寻的话,眼球肯定是从左边往右边扫最快,所以——方向向右

 

 ④找到 小于t的最后一个数:

//第一个小于,应当向左收敛
int l=L,r=R,m;
while(l<=r)
{
m=l+(r-l)/2;
if(a[m]>=t)r=m-1; //未达到FLAG,区间向左收敛
else if(a[m]<t)l=m+1; //触发FLAG,怂,退回来
}
return r<L?-1:r; //假设所有的数字都大于等于t,也就是永远触发不了FLAG,
//r就会不断左移,出现下溢
 设立的FLAG:小于t的第一个数,遥想一下,给一个数组,我们肉眼去扫寻的话,眼球肯定是从右边往左边扫最快,所以——方向向右

 

 ⑤找到 小于等于t的最后一个数: 

//第一个小于等于,应当向左收敛
int l=L,r=R,m;
while(l<=r)
{
m=l+(r-l)/2;
if(a[m]>t)r=m-1; //未达到FLAG,区间向左收敛
else if(a[m]<=t)l=m+1; //触发FLAG,怂,退回来
}
return r<L?-1:r; //假设所有的数字都大于t,也就是永远触发不了FLAG,
//r就会不断左移,出现下溢
 设立的FLAG:小于等于t的第一个数,遥想一下,给一个数组,我们肉眼去扫寻的话,眼球肯定是从右边往左边扫最快,所以——方向向右

 

 ⑥是否存在数字t,返回第一个t: 

//第一个等于,应当向右收敛
int l=L,r=R,m;
while(l<=r)
{
m=l+(r-l)/2;
if(a[m]<t)l=m+1; //未达到FLAG,区间向右收敛
else r=m-1; //触发FLAG,怂,退回来
}
return (l<=R&&a[l]==t)?l:-1; //向右收敛都要考虑上溢
//还要考虑:虽然没有出界,但可能根本不存在此数t
 这个查找数字的思路和①挺像的,所以如果这个中间的数字比 t 要小那说明肯定在右边;如果这个中间的数字比 t 要大那说明肯定在左边,这一点就不再扯原因啦。

那还需要注意的一点是,如何体现出我们要找的“第一个”这个特质呢?这么思考可能会有帮助:我要寻找的是第一个 t,那再递增的序列中,就是说要找最左边的 t 呗!(我们先假设存在)也就是说,假设我们在序列中发现了一个 t ,那也不一定是第一个——可能左边还有 t 来着!(不要说你一眼就看出来它左边还有没有别的 t ,反正机器人是看不出来的,它只看见了眼前的 t ,不然还写啥代码嘞?)所以说,如果我们发现中间的数字刚好等于 t ,那可能左边还有 t 呀,那左边的那个 t 不才更可能是“第一个”嘛! 所以我们就要往左边去找去!选择左区间!

如果我们在查找的过程中,发现了a[m]恰好等于t,这个时候也要往回退(向左边收敛),因为可能当前遇见的“t”不是第一个!故设立的FLAG:大于等于t的第一个数,所以我们是要从左边往右边找——方向向右

 

 ⑦是否存在数字t,返回最后一个t: 

//最后一个等于,应当向左收敛
while(l<=r)
{
m=l+(r-l)/2;
if(a[m]>t)r=m-1; //未达到FLAG,区间向左收敛
else l=m+1; //触发FLAG,怂,退回来
}
return (r>=L&&a[r]==t)?r:-1;//向左收敛都要考虑上溢
//还要考虑:虽然没有出界,但可能根本不存在此数t
 此时和上述类似,如果我们在搜索的时候,发现了a[m]恰好等于t,这个时候也要往回退(向右边收敛),因为可能当前遇见的“t”不是最后一个!故设立的FLAG:小于等于t的第一个数,所以我们是要从右边往左边找——方向向左

 

  推荐看的顺序是,每两个一起比对,这样会更好的发现FLAG的设立关系。设立的FLAG实际上就是我们的“目标条件”,没满足目标条件知识,我们的二分区间都会不断向前收敛;一旦达到了FLAG的要求,我们就马上回退,因为可能收敛过头了。

 

  所以二分的写法,实际上就是写出满足FLAG的情况下,回退的方向确定,就知道如何回退了,因为另一个情况就是反操作呗。

  哎,个人语文不行,叙述的可能还是乱七八糟啊。

  在末尾给一下总结。

 

  总结:

   查找大于/大于等于t的数,方向向右,返回l,考虑l上溢。

   查找小于/小于等于t的数,方向向左,返回r,考虑r下溢。

   查找第一个t, FLAG使用大于等于,方向向右,返回l,考虑l上溢,还要专门考虑此数存不存在。
   查找最后一个t,FLAG使用小于等于,方向向左,返回r,考虑r下溢,还要专门考虑此数存不存在。

       

 

    写的很菜,若有瑕疵,请指正。/(ㄒoㄒ)/

转载于:https://www.cnblogs.com/buaaliang/p/11373102.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值