【第三课】整数二分(acwing-789-含思路详解,死循环问题,while循环出口问题)

 做了力扣704的二分发现有acwing更全面的模板之后,趁热打铁,把这个学了。

目录

704思路的误用(可跳)

789暴力解法 

789二分解法

1.while()循环出口的判断条件left不能取等:<>

2.模板二中mid=(left+right+1)/2:


看到这道题,本想用力扣704那道题的解法,即,首先通过二分找到目标元素,即mid值所对的元素,确定了起始位置,然后再写一个函数计算终止位置,具体实现是,累计目标元素出现的次数,再加上begin的初值也就得到了end的值。

根据上面的想法,然后我就兴致冲冲的写了一堆bug,,,

在不断的调试代码中,我终于纠正到了最后一个bug,也终于发现这种方法是不可行的,最重要的是终于发现了704那道题的二分和我们的acwing模板例题的不同。唉。。。。

704思路的误用(可跳)

自然是想着看用704那道题的二分思路,照葫芦画瓢的写出找起始位置的函数 。

先展示find错误代码!!!

void find(int a[],int k,int n)
{
    int left=0;
    int right=n-1;
    int begin=-1,end=-1;//初始化为-1 -1 当while循环结束,值没有发生改变就表明数组中不存在目标元素(是不是还挺合理[doge 苦笑])
    while(left<=right)
    {
        int mid=left+(right-left)/2;
        if(a[mid]==k)
        {
            begin=mid;
            end=begin+find2(begin,right,a,k);//根据目标元素的个数 得到end的值
            break;
        }
        else if(a[mid]<k)
        {
            left=mid+1;
        }
        else
        {
            right=mid-1;
        }
    }
    cout<<begin<<" "<<end<<endl;
}

接着不就是end值也就是数组中目标元素个数的求解嘛。又是好长时间的修改调试之后。

写出了如下这个我怎么看也没问题的find2代码

int find2(int begin,int right,int a[],int k)
{
    int i=0;
    while(begin<=right)
    {
        if(a[begin++]==k)//begin先使用后++ 说明i所得到的是k的总个数 
        {
            i++;
        }
    }
    return i-1;//但我们计算终止位置的时候,是用begin后有几个k决定的 
}

结果执行,还是不对。。。

于是我明白了,问题在于,find函数所找到的元素,并不一定是起始元素的位置!!!!

ok,正片开始 


 由于704题目要求,当然如果你没看过我的那篇题目详解或者没做过那道题,可能不知道或者忘记了,没关系,再回顾一下那道题的题干。

仔细看看清楚,这道题所求解的,是在已知升序数组中是否存在目标元素。

写出的函数所实现的功能是,通过二分查找,判断数组中是否存在目标元素。

并不具有普适性(毕竟哪有那么多像这道题一样的二分基础题对吧?),对于更复杂的问题就需要更加高级的二分算法。


789暴力解法 

嗯,为了像之前一样完全展示我的做题过程,我还是先展示 发现704思路不适用时,我是如何思考的。

发现上述查找第一个目标元素的功能无法用类似704的方法实现,于是很自然想到,既然数组是升序的,那就遍历吧。

这就不再多说了吧,自然是暴力实现的。我将必要的解释写在注释里,方便理解。

暴力解法代码如下

#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int n, q;
int find2(int begin, int right, int a[], int k) // 找end
{
    int j = 0; // 表示目标元素出现的次数
    while (begin <= right)
    {
        if (a[++begin] == k) // begin前置++是因为我们想得到的是begin之后有多少个目标元素,不包含本来就已经有的这一个
        {
            j++;
        }
    }
    return j;
}
void find(int a[], int k, int n) // 查找数组中遇到的第一个目标元素
{
    int left = 0;
    int right = n - 1;
    int begin = -1, end = -1; // 令初始值为-1 -1 当数组中没有目标元素时直接输出
    while (left <= right)
    {
        if (a[left] == k) // 只要找到了数组中第一个目标元素,就得到了begin
        {
            begin = left;
            end = begin + find2(begin, right, a, k); // 得到begin之后找end
            break;                                   // 那么这个循环得功能就已经实现了,break跳出循环即可,避免给begin多次赋值而导致错误
        }
        left++; // 必须在这里++而不能在前面if语句里,是因为如果在if语句里++,会导致begin赋值错误
    }
    cout << begin << " " << end << endl;
}
int main()
{
    cin >> n >> q;
    int i = 0; // 表示数组下标从零开始
    int j = n;
    while (j--) // 用j来代替n做遍历,避免下面find函数的参数n值得改变出现错误
    {
        cin >> a[i++];
    }
    int k = 0; // 目标元素
    while (q--)
    {
        cin >> k;
        find(a, k, n);
    }
    return 0;
}

嗯,整整50行代码。 

观察发现这样写时间复杂度是O(nq)

这就不再赘述了。我们快开始看二分解法吧。 


789二分解法

我们首先要明确,不管是704那种简单的二分查找,还是像这道题一样复杂的查找范围的二分,我们的目的都是:

通过二分不断更新mid的值,直到得到mid=k ,当k存在于数组中时,mid最终的值就是我们想要的 二分的结果或者说是答案。

但是,即使k并不存在于数组中,二分算法仍然会计算搜索区间的中点mid,然后根据a[mid]与目标元素的大小关系来更新搜索区间的边界,也会得到一个mid值。

所以无论k是否存在,二分一定是有结果的。

如果使用像704那道题的说法:通过二分查找,判断数组中是否存在目标元素。

那么这道题就应该是:一组元素中存在一个分界线,分界线以前满足这个性质,分界线以后不满足这个性质。寻找 左边界(即 数组中从左向右看 第一个>=k的元素) ,即模板一;寻找右边界(即 数组中从左向右看 最后一个<=k的元素 ),即模板二。由此得到数的范围。

关于这两个模板解释如下图

好啦,二分算法的思想就想明白了。

另外这道题还需要注意的:

1.while()循环出口的判断条件left<right不能取等:

我们如何会想到取等?因为在704里,是可以取等的对吧?并且我们给出了清晰的可以取等的原因,为什么这里不行了呢?

那道题我们是把二分函数写在了主函数之外,我们找到了mid值,直接return,就跳出了while循环,不存在死循环的问题。

但是这里我们采用把二分过程写在主函数里,倘若条件为left<=right,当left+1=right时,数组中有两个元素(a[left]和a[right]),mid=left,且恰好满足a[mid]>=k,更新了right=mid也就=left,那么这时,left=right,倘若循环条件真的是left<=right,此时仍然符合,就会继续进入循环,但是之后的循环根本不会再改变范围,所以会陷入死循环。

2.模板二中mid=(left+right+1)/2:

为什么需要加1呢?

这是为了更精确的找到最后一个符合条件的元素,我们尽量把左区间向后找。

假设:我们要在数组【1,2,2,3】中查找2的最后一个出现位置。初始时,搜索区间为【0,3】,此时mid=left+right>>1 =1,所以我们将搜索区间更新为【1,3】此时,搜索区间缩小了。

在下一次迭代中,mid=2,搜索区间更新为【2,3】。此时,搜索区间再次缩小。

然而,在下一次迭代中,mid仍然等于2,搜索区间为【2,3】,并未缩小,之后不管循环多少次,虽然数组中还有两个元素,但mid总是等于2,left总是等于2,陷入了死循环,得不到我们想要的值。

所以我们把mid设置为left+right+1>>1,使其更靠近后半部分,与我们想找的“最后一个”形成一种促进效果。

而模板一中不需要,是因为mid的值不+1会更靠近搜索区间的左边界。这样,在更新搜索区间的边界时,搜索区间就会更多地向前移动,从而更容易找到目标元素的第一个出现位置

嗯,关于模板二到底是按照 避免死循环的原因 还是 更容易正确的找到目标这个原因来解释mid偏移1,我还不能准确的判断,希望指点,非常感谢。

好啦,注意点也说完了,下面就是代码实现了。

#include <iostream>
using namespace std;
const int N = 100000;
int a[N];
int n, q;
int main()
{
    cin >> n >> q;
    // 输入数组
    for (int i = 0; i < n; i++)
    {
        cin >> a[i];
    }
    // 开始执行询问
    while (q--)
    {
        // 输入目标元素
        int k;
        cin >> k;
        // 定义左右边界
        int left = 0, right = n - 1; // 位置从零开始

        // 开始二分边界
        while (left < right) // 注意不可取等
        {
            int mid = left + (right - left) / 2;
            // 根据题目需要,首先要找到数组中 出现第一个目标元素 的位置---查找不小于目标值的第一个位置

            if (a[mid] >= k)    
                right = mid;    
            else                
                left = mid + 1; 
        }

        // 循环出口是right=left=mid
        // 通过上述二分:不论数组中k是否存在,都会得到一个mid值
        // 所以我们要先判断该值是否等于k

        if (a[left] != k) // 说明数组中不存在k
            cout << "-1 -1" << endl;
        else // a[mid]=k时,说明起始位置已经找到,接下来要找终止位置
        {
            cout << left << " "; // 记得输出起始位置 注意空格

            left = 0;
            right = n - 1;
            // 查找终止位置 ---查找不大于目标值的最后一个位置
            while (left < right)
            {
                int mid = left + right + 1 >> 1; // 为了避免死循环需要+1再除二
                if (a[mid] <= k)                 
                    left = mid;
                else 
                    right = mid - 1;
            }
            cout << left << endl;
        }
    }
    return 0;
}

 ok,好长【笑哭】

整数二分就说到这里了。 

有问题欢迎指出,非常感谢!!

也欢迎交流建议奥。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值