二分查找介绍
- 二分的本质是找到区间的一个分界点,使得包含分界点的区间满足某个性质,不含分界点的区间不满足这个性质,二分查找是基于有序序列的查找算法。
- 二分查找的高效之处在于,每一步都可以去除当前区间的一半元素,因此其时间复杂度时O(logn)。
- 举个例子:图书馆自习的时候,一女生背着一堆书进阅览室,结果警报响了,大妈让女生看是哪本书把警报弄响了,女生把书倒出来,一本一本的测。大妈见状急了,把书分成两份,第一份过了一下,响了。又把这一份分成两份接着测,三回就找到了,大妈用鄙视的眼神看着女生,仿佛在说 O(N)和 O(logN)都分不清。
- 二分思想:每次将范围缩小一半 , 确定范围内答案会被覆盖到 , 当长度为1时,就是答案,整数二分模板要保证有解,不同题目可能会无解
基本的二分查找应用
查找序列中是否存在某条件的元素
例如二分查找常用于解决最大值最小化 和 最小值最大化问题
二分查找解释
- 首先,区间是有序的有单调性的,但二分本质不是单调性,有单调性一定可以二分,但可以二分不一定非要有单调性。
若要查到的一个数比目标值大,则右半部分的值也比目标值大,那么右半部分就可以舍弃了。同样查找的值比较小,就可以舍弃左半边。 - 二分查找要满足
1.当前待查找序列,肯定包含目标元素 2.每次待查找序列的规模都会变小。 - 图解(借鉴于acwing yxc)
要找到一个性质可以将区间一分为二,一半满足一半不满足要求,则可以寻找绿色或者红色的边界。当在不同的边界,那么就产生不同的模板
对于红色边界
是来寻找小于等于目标值的数
绿色边界
是来寻找大于等于目标值的数,因此左边界 l 要一直向右
这里为什么要mid = l + r +1 >> 1呢,相加的整数的除法是向下取整,当l = r - 1,若补不上1,向下取整 mid = l,会陷入死循环。
补上后mid会等于r,即[r,r]。
整数二分查找模板
二分查找模板有两个 , 一是尽量往左找,二十尽量往右找
整数的二分查找会更ex人 , 看起来很简单 ,但经常会出错,要么纠结在mid是否+1 , 要么纠结是否加等号。一个简便方法 , 这里坐标记left -> l,right ->r.
本质一点就是观察更新区间的时候,写的是l = mid还是r = mid,如果是第一种,那么就是区间向右缩小,便是求末位置,反之便是求初始位置。
//模板一
while (l < r)
{
int mid = l + r >> 1; //(l+r)/2
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
模板二
while (l < r)
{
int mid = l + r + 1 >> 1; //(l+r+1)/2
if (check(mid)) l = mid;
else r = mid - 1;
}
简单总结下这两个模板
只要是往左找答案,就用第一个模板,mid不用加一,r=mid,l减一;
只要是往右找答案,就用第二个模板,mid要加一,l=mid,r要减一;
二分查找模板题
查找元素在序列中的始末位置,很典型的二分查找例题,查找初始位置,套用模板一,查找末位置,套用模板二。
# include <iostream>
using namespace std ;
const int N = 1e5 + 10 ;
int a[N];
int main(){
int n , m , k;
cin >> n >>m;
for (int i = 0 ; i < n ; i ++)
cin >> a[i];
while ( m -- ){
cin >> k ;
int l = 0 , r = n - 1 ;
while ( l < r ){//先查找第一次出现的位置
int mid = r + l >> 1;
if (a[mid] >= k) r = mid ;
else l = mid + 1 ;
}
if (a[l] != k) cout << "-1 -1" << endl;// 查找不到元素,直接结束查找末位置
//当始位置能够被查到,继续进行查找末位置
else {
cout << l << " ";
l = 0 , r = n - 1 ; // 查找大于等于目标值的数,mid要加以避免向下取整死循环
while ( l < r ){
int mid = r + l + 1 >> 1 ;
if ( a[mid] <= k ) l = mid ;
else r = mid - 1 ;
}
cout << l << endl ;
}
}
return 0 ;
}