zoukankan      html  css  js  c++  java
  • 【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ㄒ)/

  • 相关阅读:
    SNMP概述–运维必知的协议基础
    关于多线程情况下Net-SNMP v3 版本导致进程假死情况的跟踪与分析
    关于snmp octet string和普通string问题
    SVN状态说明
    SNMP mib文件说明
    Linux之 proc文件系统
    django .all .values .value_list 数据库获取数据
    Django form验证
    JSONP实现
    iframe和form表单实现ajax请求上传数据
  • 原文地址:https://www.cnblogs.com/buaaliang/p/11373102.html
Copyright © 2011-2022 走看看