浅谈-二分查找

一、什么是二分查找?

1、我们首先引入这样一个问题:如果规定某一科目成绩分数范围:[0,100],现在小明知道自己的成绩,他让你猜他的成绩,如果猜的高了或者低了都会告诉你,用最少的次数猜出他的成绩,你会如何设定方案?(排除运气成分和你对小明平时成绩的了解程度)

①最笨的方法当然就是从0开始猜,一直猜到100分,考虑这样来猜的最少次数:1(运气嘎嘎好),100(运气嘎嘎背);

②其实在我们根本不知道对方水平的条件下,我们每一次的猜测都想尽量将不需要猜的部分去除掉,而又对小明不了解,不知道其水平到底如何,那么我们考虑将分数均分,

将分数区间一分为2,我们第一次猜的分数将会是50,当回答是低了的时候,我们将其分数区域从【0,100】确定到【51,100】;当回答高了的时候,我们将分数区域确定到【0,49】。这样一下子就减少了多余的50次猜想(从0数到49)(或者是从51到100)。

③那么我们假设当猜完50分之后答案是低了,那么我们需要在【51,100】分的区间内继续猜小明的分数,同理,我们继续折半,第二次我们将猜75分,当回答是低了的时候,我们将其分数区域从【51,100】确定到【76,100】;当回答高了的时候,我们将分数区域确定到【51,74】。这样一下子就减少了多余的猜想(从51数到74)(或者是从76到100)。

④就此继续下去,直到回复是正确为止,这样考虑显然是最优的、

2、那么我们最多将会有多少次查询呢?

我们不妨写出一份代码来测试一下:

#include<stdio.h>
int main()
{
    for(int y=0;y<=100;y++)//定义一个分数
    {
        int l=0;
        int r=100;
        int mid;
        int cont=0;//统计猜了多少次
        while(r>=l)
        {
            cont++;
            mid=(l+r)/2;
            if(mid>y)r=mid-1;//如果当前猜想值大了,那么接下来要在区间【l,mid-1】范围内确定下一个要猜的值
            else if(mid<y)l=mid+1;<span style="font-family: Arial, Helvetica, sans-serif;">//如果当前猜想值小了,那么接下来要在区间【mid+1,r】范围内确定下一个要猜的值</span>
            else break;//猜中了之后就要跳出啦
        }
        printf("y:%d cont:%d ",y,cont);
        if(y%4==0&&y!=0)printf("\n");
    }

得到的结果很明显:



最多将会操作7次,其实因为每一次我们都抛掉当前确定的区间的一半的区间作为不可能解部分,那么相当于求最多操作次数,就是在区间内,最多将有多少个一半可以抛去、那么就是将100一直除以2,直到不能除为止。

那么这个运算过程,其实就是相当于求了一个log2(100)≈7。


3、那么二分查找的时间复杂度:O(logn);n表示区间长度。


二、二分查找的经典应用以及适用范围;


1、我们再引入这样一个问题:如果给你一个长度为N(1<=N<=1e5)的序列,一共有q个查询(1<=q<=1e5),每一次查询输入一个值x(0<=x<=1e18),让你找第一个比x大的数,保证有解。

时间限制为1000ms


样例输入:

10

6 7 8 9 10 1 2 3 4 5

1

3


①如果我们用最简单的想法来做的话:

每次查询都对应一层for扫序列,记录所有比x大的数,从中维护一个最小值。

其时间复杂度为O(qn),最坏情况操作次数为10^10,然而一般OJ1000ms能够承受的操作次数约为10^8,显然这样做就会超时。

那么我们如果在扫数组的过程中能够降低复杂度,想到二分查找的话,那么时间复杂度将会变成:O(qlongn),最坏情况操作次数约为10^6左右,现在显然就不会超时了。


那么考虑二分查找:

③如果我们按照上述猜分数的过程来猜这个比x大的第一个数的话,首先我们一定要确定这个序列是递增或者是递减的。因为假设我们现在在当前序列进行上述过程的二分:

区间长度显然为10,那么我们看看中间的数(第五个):10和3的关系,明显10>3,但是我们接下来要确定的区间范围呢?是【0,4】好,还是【6,10】好呢?虽然我们眼睛是能够看出来的,但是问题交给代码和电脑,他们却无从下手。

但是如果我们能够保证序列是递增的,那么这个时候序列为:

1 2 3 4 5 6 7 8 9 10

区间长度显然为10,那么我们看看中间的数(第五个):5和3的关系,明显5>3,那么我们接下来如何确定下一个要找的区间范围呢?因为第一个第二个第三个第四个数都小于第五个数,那么明显我现在要找的值应该在【0,4】中,或者就是5本身,那么这个时候我们将值5记为一个可行答案。而并非【6,10】;这个时候我们确定下来区间【0,4】之后,依次类推,一直二分下去,如果a【mid】>x,那么我们就将这个数记做答案,那么最后一次记录到的值就是答案。

代码:

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int a[1000400];
int main()
{
    int n;
    while(~scanf("%d",&n))
    {
        for(int i=0;i<n;i++)
        {
            scanf("%d",&a[i]);
        }
        sort(a,a+n);
        int q;
        scanf("%d",&q);
        while(q--)
        {
            int x;
            scanf("%d",&x);
            int l=0;
            int r=n-1;
            int mid;
            int ans;
            while(r-l>=0)
            {
                mid=(l+r)/2;
                if(a[mid]>x)
                {
                    r=mid-1;
                    ans=a[mid];
                }
                else l=mid+1;
            }
            printf("%d\n",ans);
        }
    }

④总结上述过程:

首先将序列变成递增/递减的带有单调性的序列之后,再进行二分查找,对可行解进行记录,最后一次记录的可行解就是答案。


⑤那么想要对一个序列进行二分查找,那么我们首先一定需要满足这个序列带有单调性。(二分查找的使用范围)


希望文章对大家理解二分查找会有一个辅助性,这里再列出两个需要二分查找的问题大家可以对应趁热打铁一波:

转载于:https://blog.csdn.net/mengxiang000000/article/details/52751310

猜你喜欢

转载自blog.csdn.net/yinni11/article/details/81060237
今日推荐