1、算法思路
二分算法不仅仅只适用于具有单调性的数组上, 其本质是
对于一个数组,左边(右边)符合某条件A,右边(左边)符合某条件B,并且没有元素可以同时满足条件A和条件B,我们都可以通过二分,找到符合条件A的或者B的分界点。
二分又可以分成整数二分和浮点数二。
对于整数二分,我们可以找到两个分界点,如下图。为了寻找这两个分界点,我们在处理的时候会有不同。
而对于浮点数二分,只需要在足够小的区间内任意选取一个点作为分界点。
整数二分具体步骤
-
定义check()函数,用来判断某元素是否符合某条件
-
判断符合该条件的元素位于整个区间的哪一边
-
具体处理,假设符合该条件的分界点位置为x,l为当前分析区间的左端点,r为右端点
3.1 如果是位于整个数组的左边
- 取中间位置mid = (l + r +1) / 2 (整数除法)
- 如果mid元素符合check函数,则有l <= mid <= x <= r,将l设为mid,则新的l,r区间中,仍然包含着x。
- 如果mid元素不符合check函数,则有l <= x < mid < r,将r设为mid - 1(因为mid不符合check函数而x符合check函数,mid必不能等于x),则新区间l,r中,仍包含着x。
- 当l == r时,则找到了x
3.2 如果是位于整个数组的右边
- 取中间位置mid = (l + r) / 2 (整数除法)
- 如果mid元素符合check函数,则有l <= x <= mid <= r,将r设为mid,则新的l,r区间中,仍然包含着x。
- 如果mid元素不符合check函数,则有l <= mid < x < r,将l设为mid + 1(因为mid不符合check函数而x符合check函数,mid必不能等于x),则新区间l,r中,仍包含着x。
- 当l == r时,则找到了x
可以看到,不断缩小的[l,r]区间中始终包含着x,所以当l==r的时候,就找到了x
浮点数二分具体步骤
-
定义check()函数,用来判断某元素是否符合某条件
-
判断符合该条件的元素位于整个区间的哪一边
-
取中间位置 mid = (l + r) / 2 (浮点数除法)
3.1 如果是位于整个数组的左边
- 如果mid元素符合check函数,将l设为mid,则新的l,r区间中,仍然包含着x。
- 如果mid元素不符合check函数,将r设为mid ,则新区间l,r中,仍包含着x。
3.2 如果是位于整个数组的右边
- 如果mid元素符合check函数,将r设为mid,则新的l,r区间中,仍然包含着x。
- 如果mid元素不符合check函数,将l设为mid ,则新区间l,r中,仍包含着x。
-
当 r - l 足够小时时,则区间内任何一个点都可以当做结果
可以看到,不断缩小的[l,r]区间中始终包含着x,所以当r - l足够小的时候,区间内任何一个数都可以算作结果
2、模板代码
以下代码模板参考Acwing
bool check(int t);
//整数二分
//符合条件的元素位于区间右边
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
//符合条件的元素位于区间左边
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;
}
//浮点数二分
//符合条件的元素位于区间右边
double bsearch_3(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
//符合条件的元素位于区间左边
double bsearch_4(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) l = mid;
else r = mid;
}
return l;
}
1、代码解释
我认为二分就是保证目标位置x一直在区间中的情况下,不断地缩小区间,从而找到这个位置。
注意浮点二分中,最后保留的区间精度取决于题目要求保留的答案位数,需要多保留两位。
2、具体示例
对于一个数组,我们寻找大于3的第一个数字的位置
1 2 3 3 4 4 5
我们看到我们想寻找的位置是在整个数组的右边
元素值 | 1 | 2 | 3 | 3 | 4 | 4 | 5 |
---|---|---|---|---|---|---|---|
元素位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
查找区间 | l | r |
mid = (l + r) / 2 = 3
第三个位置值为3 不符合大于3的要求,l = mid + 1
元素值 | 1 | 2 | 3 | 3 | 4 | 4 | 5 |
---|---|---|---|---|---|---|---|
元素位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
查找区间 | l | r |
mid = (l + r) / 2 = 4
第四个位置的值为4 符合大于3的要求,r = mid
元素值 | 1 | 2 | 3 | 3 | 4 | 4 | 5 |
---|---|---|---|---|---|---|---|
元素位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
查找区间 | l r |
此时l == r可以返回位置 4
3、边界问题分析
1. 避免递归时区间无限划分问题
两个mid的取法不一样,一个是上取整,一个是下取整,是因为需要避免无限划分的问题。
比如符合该条件的元素位于整个区间右边的情况下,符合条件的时候,r = mid , 否则l = mid + 1。
如果是上取整,也就是mid = (l + r + 1 ) / 2。当 l和r相差1的时候,也就是r比l大1的时候,mid = r了,此时如果符合条件的时候,r不会变,区间仍然是l,r,会一直循环下去。
如果是下取整,上述情况时,mid = l,区间会变成(l,l)从而结束循环。
3、 算法复杂度分析
1.时间复杂度分析
每次循环都会把区间缩小一半,将区间缩小到1需要缩小 l o g 2 N log_2N log2N次,所以时间复杂度为 O ( l o g 2 N ) O(log_2N) O(log2N)