数据结构与算法——二分
最近leetcode每日一题经常出二分的题目,正好对前段时间学过的二分进行一些总结,首先这里要明确的一点是,二分的本质并不是单调性,而是通过某种条件将整个区间划分成满足条件和不满足条件的两端即可进行二分查找。
在二分这个专题,主要有两种类型的划分方式,一种是整数划分,一种是浮点数划分,前一种一般是我们最熟悉的二分查找的题型,也是出题比较灵活考的比较多的一种,后一种主要是为控制实数精度而设置的浮点数二分法(建议用double
,float
有时候会出现精度丢失)。
整数二分
这里我们拿一道经典例题来给出我们二分的两个十分精妙的模板。AcWing789. 数的范围
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
while(l < r)
{
int mid = l + r >> 1; // 如果写r=mid则这里不需要+1
if(check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l; //退出时 l与r相等
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while(l < r)
{
int mid = l + r + 1 >> 1;
if(check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
浮点数二分
浮点数二分考的比较少,主要是实现对于高精度答案的控制。AcWing 790. 数的三次方根
这里有个小bug需要提醒一下,我们以求二次方根为例,我们可以知道,
x
=
0.01
x=0.01
x=0.01时,
x
=
0.1
>
x
\sqrt{x}=0.1>x
x=0.1>x,所以当我们把二分的区间设置成
[
0
,
x
]
[0,x]
[0,x]时,我们则无法找到答案,所以设置区间的时候我们最好可以设置大一点的区间,或者设置成
[
0
,
max
(
1
,
x
)
]
[0,\max(1,x)]
[0,max(1,x)]
bool check(double x) {/* ... */} // 检查x是否满足某种性质
double bsearch_3(double l, double r)
{
const double eps = 1e-8; // eps 表示精度,取决于题目对精度的要求
while(r - l > eps)
{
double mid = (l + r) / 2; // 对于浮点数二分则不需要考虑+1的问题
if(check(mid)) r = mid;
else l = mid;
}
return l;
}
STL
做题当中手写二分的题其实比较少,基本上记忆上述模板就能解决所以的问题,同时我们在日常做题的时候,为了方便,我们通常是使用STL当中的函数来实现二分,且支持vector,map,set等操作,还有结构体大小比较,这里就有三个一定要记住的API。头文件引入:#include<algorithm>
binary_search
功能:二分查找某个元素是否出现。
返回值:在数组中以二分法检索的方式查找,若在数组(要求数组元素非递减)中查找到indx元素则真,若查找不到则返回值为假。
用法实例:
a.数组用法
int a[100]= {4,10,11,30,69,70,96,100};
int b=binary_search(a,a+9,4);//查找成功,返回1
cout<<"在数组中查找元素4,结果为:"<<b<<endl;
b.vector用法
vector<int> res = {1,2,3};
cout<<binary_search(res.begin(),res.end(),3)<<endl;
lower_bound
功能:查找非递减序列[first,last) 内第一个大于或等于某个元素的位置。
返回值:如果找到返回找到元素的地址否则返回数组边界的下一个元素的地址。(这样不注意的话会越界,小心)
用法实例:
int a[100]= {4,10,11,30,69,70,96,100};
int d=lower_bound(a,a+9,10)-a;
cout<<"在数组中查找第一个大于等于10的元素位置,结果为:"<<d<<endl;
int e=lower_bound(a,a+9,101)-a;
cout<<"在数组中查找第一个大于等于101的元素位置,结果为:"<<e<<endl;
b.vector用法
vector<int> res = {1,2,3};
vector<int>::iterator it = lower_bound(res.begin(),res.end(),3);//返回迭代器的位置
//如果不存在,迭代器的位置会返回res.end()
if(it==res.end()) cout<<"不存在"<<endl;
else cout<<"求出下标:"<<(it - res.begin())<<endl;
//也可以添加偏移量
vector<int>::iterator it = lower_bound(res.begin()+1,res.end(),3);
upper_bound
功能:查找非递减序列[first,last) 内第一个大于某个元素的位置。
返回值:如果找到返回找到元素的地址,否则返回数组边界的下一个元素的地址。(同样这样不注意的话会越界,小心)
用法实例:
int a[100]= {4,10,11,30,69,70,96,100};
int d=upper_bound(a,a+9,10)-a;
cout<<"在数组中查找第一个大于等于10的元素位置,结果为:"<<d<<endl;
int e=upper_bound(a,a+9,101)-a;
cout<<"在数组中查找第一个大于等于101的元素位置,结果为:"<<e<<endl;
b.vector用法
vector<int> res = {1,2,3};
vector<int>::iterator it = upper_bound(res.begin(),res.end(),3);//返回迭代器的位置
//如果不存在,迭代器的位置会返回res.end()
if(it==res.end()) cout<<"不存在"<<endl;
else cout<<"求出下标:"<<(it - res.begin())<<endl;
//也可以添加偏移量
vector<int>::iterator it = upper_bound(res.begin()+1,res.end(),3);
经典例题
这里我顺便给出这两天的每日一题的解题方案,里面还涉及到了一个防止溢出的二分处理trick。
猜数字大小
/**
* Forward declaration of guess API.
* @param num your guess
* @return -1 if num is lower than the guess number
* 1 if num is higher than the guess number
* otherwise return 0
* int guess(int num);
*/
class Solution {
public:
int guessNumber(int n) {
int l = 1;
int r = n;
while(l<r)
{
int mid = l + (r - l >> 1);//防止溢出
if(guess(mid)<=0)
r = mid;
else
l = mid + 1;
}
return r;
}
};
class Solution {
public:
int peakIndexInMountainArray(vector<int>& arr) {
int l = 0;
int r = arr.size() - 1;
while(l<r)
{
int mid = l + r >> 1;
if(arr[mid]>arr[mid+1]) r = mid;
else l = mid+1;
}
return l;
}
};
这是三叶姐姐的二分经典题型汇总,大家也可以参考一下。