👑1 原理引入
假设我们现在有这样一个从小到大排好序的数组
int array=[1,2,2,3,3,4];
题目:“想要寻找 这个元素在数组中的位置”。 我可以怎么样去做呢?
最简单的想法,我们是不是可以从头开始遍历数组,如果找到等于3的元素就记录该元素的下标。这样子的时间复杂度毫无疑问是,这种方式叫做“线性搜索”那有没有更快的方式呢。
当然有,这一类查找元素的方式统称为“搜索算法”,常见如下:
线性搜索(Linear Search):
- 时间复杂度:𝑂(𝑛),其中 𝑛是列表中元素的数量。
- 最坏情况下,需要遍历整个列表才能找到目标元素,因此时间复杂度为线性的。
二分搜索(Binary Search):
- 时间复杂度:𝑂(log𝑛),其中 𝑛n是数组的长度。
- 每次比较都将搜索范围减半,因此时间复杂度为对数级别的。
插值搜索(Interpolation Search):
- 时间复杂度:平均情况下为 𝑂(loglog𝑛),最坏情况下为 𝑂(𝑛)。
- 插值搜索根据目标值的大致位置进行估计,可以更快地定位目标值,但在数据分布不均匀的情况下,可能退化为线性搜索。
斐波那契搜索(Fibonacci Search):
- 时间复杂度:𝑂(log𝑛)。
- 斐波那契搜索使用黄金分割比例来确定搜索位置,具有对数级别的时间复杂度。
根据算法竞赛中和以后的GESP考级内容,我们只需要学习“二分查找”也叫“二分搜索”算法即可,其他的只需要了解即可。且我们可以看出“二分查找”是效率最高的算法之一,他的使用广泛成都也是最高的。
假设此时的情景为,需要我们找到第一个>=3的数字,使用二分算法顾名思义,首先把整个区间分为两半,左边是[1, 2, 2],右边是[3, 3, 4]的区间 如图:
此时,我们发现中间难以定义,准确来说中间数是和
,我们遇到这种情况一般选择前一位
作为中间数,因为当我们求中间数的时候一般使用索引的方式,首先获得中间数的索引,也就(是最左边的索引
l+最右边的索引
)➗2,此时的索引指向中间数:
mid = l + r >> 2;
// 左索引 + 右索引 除以 二 由于位运算得到的是整数5➗2= 2.5(int) = 2
这个时候我们可以进行一个判断,假设数组名是arr,arr[mid]=2,2小于我们要查找的数3,由于数组是递增的,我们是不是就可以肯定 mid=2,arr[mid]=2,这个中间数一定位于数3的左边,此时我们只需要让,那么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的位置相邻,因此可以结束循环。
此时循环结束,找到数组中存储3的下标为3,返回3,因为找到了3这个元素的位置位于mid=3,其他3的位置,从这个3的两头出发寻找即可。
👑2 代码复现
显而易见,要实现二分查找我们需要3个工具:
① 指向区间最左边下标的
② 指向区间最右边下标的
③ 指向区间最中间的下标
④ 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, 2, 2, 3, 3, 4],,如果你要寻找第一个 >=5 的数,你会发现,整个过程都在执行l=mid,最后得到的结果中,r是等于下标6的,他明显这个时候是越界的,说明我们找不到要寻找的数字,而如果我们一开始将r赋值为n-1,也就是赋值为下标5的时候,他返回的r是5,是没有越界的,被我们当成了答案,但其实这时候我们的二分是没有答案的,就发生了错误;
- 其次,l最小值为-1,r最小值只能取到1,因为l+1!=R为循环结束条件,r最大值为n,同理则 l 的最大值为 n - 2,则(l+r)/ 2 的取值范围是 [0,n),mid的值始终位于0到n的左闭右开区间里面,不会发生越界的错误;
从刚刚我原理可以看出来二分运行的过程是一个循环, 结束条件是,由此可以写出:
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);
}
}
}