CCF-GESP5级考试——查找与排序算法之二分查找

👑1 原理引入

        假设我们现在有这样一个从小到大排好序的数组

int array=[1,2,2,3,3,4];

题目:“想要寻找 3 这个元素在数组中的位置” 我可以怎么样去做呢?

        最简单的想法,我们是不是可以从头开始遍历数组,如果找到等于3的元素就记录该元素的下标。这样子的时间复杂度毫无疑问是O(n),这种方式叫做“线性搜索”那有没有更快的方式呢。

        当然有,这一类查找元素的方式统称为“搜索算法”,常见如下:

线性搜索(Linear Search):

  • 时间复杂度:𝑂(𝑛),其中 𝑛是列表中元素的数量。
  • 最坏情况下,需要遍历整个列表才能找到目标元素,因此时间复杂度为线性的。

二分搜索(Binary Search):

  • 时间复杂度:𝑂(log⁡𝑛),其中 𝑛n是数组的长度。
  • 每次比较都将搜索范围减半,因此时间复杂度为对数级别的。

插值搜索(Interpolation Search):

  • 时间复杂度:平均情况下为 𝑂(log⁡log⁡𝑛),最坏情况下为 𝑂(𝑛)。
  • 插值搜索根据目标值的大致位置进行估计,可以更快地定位目标值,但在数据分布不均匀的情况下,可能退化为线性搜索。

斐波那契搜索(Fibonacci Search):

  • 时间复杂度:𝑂(log⁡𝑛)。
  • 斐波那契搜索使用黄金分割比例来确定搜索位置,具有对数级别的时间复杂度。

        根据算法竞赛中和以后的GESP考级内容,我们只需要学习“二分查找”也叫“二分搜索”算法即可,其他的只需要了解即可。且我们可以看出“二分查找”是效率最高的算法之一,他的使用广泛成都也是最高的。

         假设此时的情景为,需要我们找到第一个>=3的数字,使用二分算法顾名思义,首先把整个区间分为两半,左边是[1, 2, 2],右边是[3, 3, 4]的区间 如图:

         此时,我们发现中间难以定义,准确来说中间数是23,我们遇到这种情况一般选择前一位2作为中间数,因为当我们求中间数的时候一般使用索引的方式,首先获得中间数的索引,也就(是最左边的索引ll+最右边的索引r)➗2,此时的索引指向中间数: 

mid = l + r >> 2;
// 左索引 + 右索引 除以 二 由于位运算得到的是整数5➗2= 2.5(int) = 2

         这个时候我们可以进行一个判断,假设数组名是arr,arr[mid]=2,2小于我们要查找的数3,由于数组是递增的,我们是不是就可以肯定 mid=2,arr[mid]=2,这个中间数一定位于数3的左边,此时我们只需要让l=mid,那么l左边的元素便不需要再考虑,因为肯定小于3,此时数组变成这样,如下图👇

         同样的流程,mid=l+r>>2 = 2 + 5 >> 2 = 7 >>2 = 3。此时接着判断 arr[mid] = arr[3] = 3,mid此时指向的数组元素等于3,这次不同于上次,上次是l=mid,这次由于不满足条件,故而r=mid,这样r= l + 1,r和l的位置相邻,因此可以结束循环。

        此时循环结束,找到数组中存储3的下标为3,返回3,因为找到了3这个元素的位置位于mid=3,其他3的位置,从这个3的两头出发寻找即可。

👑2 代码复现

        显而易见,要实现二分查找我们需要3个工具:

① 指向区间最左边下标的 l

② 指向区间最右边下标的 r

③ 指向区间最中间的下标 mid

④ check() 判断条件,控制二分的进行。

我们使用代码创建上列条件, 但我们一开并不直接指向区间的最左边和最右边,而是l=最左边-1也就是-1,r=最右边 + 1也就是数组长度 。

特别说明:由于l 减少了1,r增加了1,所以一开始的mid并没有变化。

int a[6]={1,2,2,3,3,4};
int l=-1,r=6;//定义两个指针

为什么L的初始值为-1,R的初始值为N🎈

  1.         如果二分本来就没有结果,比如对于本文例题 [1, 2, 2, 3, 3, 4],,如果你要寻找第一个 >=5 的数,你会发现,整个过程都在执行l=mid,最后得到的结果中,r是等于下标6的,他明显这个时候是越界的,说明我们找不到要寻找的数字,而如果我们一开始将r赋值为n-1,也就是赋值为下标5的时候,他返回的r是5,是没有越界的,被我们当成了答案,但其实这时候我们的二分是没有答案的,就发生了错误;
  2.    其次,l最小值为-1,r最小值只能取到1,因为l+1!=R为循环结束条件,r最大值为n,同理则 l 的最大值为 n - 2,则(l+r)/ 2 的取值范围是 [0,n),mid的值始终位于0到n的左闭右开区间里面,不会发生越界的错误;

从刚刚我原理可以看出来二分运行的过程是一个循环, 结束条件是r= l + 1,由此可以写出:

while(l + 1 != r){
# l + 1 == r 的时候结束,因此满足l +  1 != r 的时候继续循环
}

在此之前代码不断的执行循环,进行区间逐渐缩小的二分。故而wihle循环里面的代码便是给mid复制,并且进行判断,如果符合check()就l=mid,如果不符合check()就r=mid。check()需要我们自行设计。

while ( l + 1 != r){
    # ①赋值给mid
    int mid = l + r >> 1;
    # ②判断通过, l = mid
    if (判断条件) l = mid
    # ③判断不通过, r = mid
    else r=mid
}

整体模板就是:

int L=-1,R=n;
while(L+1!=R)
{
	int mid=L+R>>1;
	if(check()) L=mid;
	else R=mid;
	//最后根据你所分左右两边区间的结果
	//选取L或者R作为结果
}

 应用到解决例题就是:

#include<iostream>
using namespace std;
int main()
{
int a[6]={1,2,2,3,3,4};
int l=-1,r=6;//定义两个指针
while(l+1!=r)
{
	int mid=l+r>>1;//(相当于(l+r)/2)
	if(a[mid]<3) l=mid;
	else r=mid;		
}
cout<<"3所在的下标为  "<<r<<endl;
return 0;
}	

3 👑算法应用【数的范围】

 

输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1

 参考过关代码:🥒

#include<iostream>
using namespace std;
const int N=1e5+5;
int n,m,q[N];
int main()
{
    scanf("%d %d",&n,&m);
    for(int i=0;i<n;i++) scanf("%d",&q[i]);
    while(m--)
    {
        int k;scanf("%d",&k);
        //寻找第一个等于K的坐标 我这边让二分的边界定为 左边为<5 右边>=5 则所求为r
        int l=-1,r=n;
        while(l+1!=r)//当l与r没有相接的时候,求边界
        {
            int mid=l+r>>1;
            //下面找第一个>=5的坐标
            if(q[mid]>=k) r=mid;
            else l=mid;
        }
        //此时得到的r是第一个>=5的坐标
        if(q[r]!=k) printf("-1 -1\n");
        else{
            printf("%d ",r);
                //现在找最后一个<=5的数字 我这边让二分的左边为<=5 右边为>5 则所求为ll
                int ll=-1,rr=n;
                while(ll+1!=rr)
                {
                    
                    int mid=ll+rr>>1;
                    if(q[mid]<=k) ll=mid;
                    else rr=mid;
                }
                if(q[ll]!=k) printf("%d\n",r);
                else printf("%d\n",ll);
            }
        
    }
    
}

  • 16
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大气层煮月亮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值