当我们要从一个序列中查找一个元素的时候,最简单无脑的方法就是顺序查找法,但由于在大数据情况下爆炸的时间复杂度而舍弃。
最常见的方法是二分查找,也称折半查找(Binary Search),它是一种效率较高的查找方法。
最近偶然看到 『LeetCode』 讨论中的大佬总结的 二分查找从入门到入睡 ,虽然文章巨长,但总结的很全,一些边界问题讲的也很细,其中包括了Y总的二分思路,非常推荐看一看!!
[l, r]
中寻找目标值 x
,二分的思想是每次将区间长度缩小一半,当l = r
时,我们就找到了目标值。mid = (l + r + 1) / 2
;mid
满足性质 check(mid)
,即 if(check(mid)) = True
,则更新区间为 [mid, r]
,即令l = mid
即可;mid
不满足性质 check(mid)
,即 if(check(mid)) = False
,则更新区间为 [l, mid - 1]
,即令r = mid - 1
即可;l >= r
,边界值为 l
。mid = (l + r) / 2
(上取整是为了防止死循环);mid
满足性质 check(mid)
,即 if(check(mid)) = True
,则更新区间为 [l, mid]
,即令r = mid
即可;mid
不满足性质 check(mid)
,即 if(check(mid)) = False
,则更新区间为 [mid + 1, r]
,即令l = mid + 1
即可;l >= r
,边界值为 l
。【模板一】
l < r
[l, r]
→ [l, mid - 1]
和[mid, r]
r = mid - 1
或者l = mid
mid
时为了避免死循环需要加1
,即mid = l + r + 1 >> 1
。bool check(int x) {/* ... */} // 检查x是否满足某种性质
int bsearch(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
【模板二】
l < r
[l, r]
→ [l, mid]
和[mid + 1, r]
r = mid
或者l = mid + 1
mid
时不需要加1
,即mid = l + r >> 1
。bool check(int x) {/* ... */} // 检查x是否满足某种性质
int bsearch(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
bool check(double x) {/* ... */} // 检查x是否满足某种性质
double bsearch(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps) // 两种写法:此时是用精度控制循环次数,直接控制循环100次也是OK的!
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
做题的顺序首先是确定 check()
函数,再进行区间划分的分析,再确定使用哪个模板。
可以看到:当 l = mid
时,我们使用模板一,且 mid = (l + r) / 2
为下取整;当 r = mid
时,我们使用模板二,且 mid = (l + r + 1) / 2
需要上取整;
l = r - 1
,即 l
和 r
只差 1
的时候,如果 mid = (l + r) / 2
下取整的话,结果是等于 l
的,则一旦 if(check(mid)) = True
,更新区间会一直陷入到 [mid, r] = [l, r]
死循环中。l = r - 1
,即 l
和 r
只差 1
的时候,如果 mid = (l + r + 1) / 2
下上取整的话,结果是等于 r
的,则一旦 if(check(mid)) = True
,更新区间会一直陷入到 [l, mid] = [l, r]
死循环中。【整数二分 - 模板题】AcWing 789. 数的范围
【思路】想要找到目标值 x
的起始坐标,可理解成找到 ≥x
的最小值,再判断找到的边界值是否与 x
相等,若不相等返回 -1
;同样,想要找到目标值 x
的终止坐标,可理解成找到 ≤x
的最大值,再判断找到的边界值是否与 x
相等,若不相等返回 -1
;
【C++代码】
#include
using namespace std;
const int N = 100010;
int n, m;
int q[N];
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
while (m -- )
{
int x;
scanf("%d", &x);
int l = 0, r = n - 1;
while (l < r)
{
int mid = l + r >> 1;
if (q[mid] >= x) r = mid;
else l = mid + 1;
}
if (q[l] != x) cout << "-1 -1" << endl;
else
{
cout << l << ' ';
int l = 0, r = n - 1;
while (l < r)
{
int mid = l + r + 1 >> 1;
if (q[mid] <= x) l = mid;
else r = mid - 1;
}
cout << l << endl;
}
}
return 0;
}
【浮点数二分 - 模板题】AcWing 790. 数的三次方根
【C++代码】
#include
using namespace std;
int main()
{
double x;
cin >> x;
double l = -100, r = 100;
while (r - l > 1e-8)
{
double mid = (l + r) / 2;
if (mid * mid * mid >= x) r = mid;
else l = mid;
}
printf("%.6lf\n", l);
return 0;
}
【模板三】相等返回
l <= r
[l, r]
→ [l, mid - 1]
和[mid + 1, r]
r = mid - 1
或者l = mid + 1
mid
时不需要加1
,即mid = l + r >> 1
。int bsearch(int l, int r)
{
while (l <= r)
{
int mid = l + r >> 1;
if (q[mid] == target) return mid;
else if (q[mid] < target) l = mid + 1;
else r = mid - 1;
}
return -1;
}
此处借鉴学习一下『 LeetCode 大佬 - yukiyama』 的总结表格,详解请参考:二分查找从入门到入睡
l < r
,结束循环条件必定是相等 l = r
终止,且二分范围一般来讲是 [0, n - 1]
,n
为数组长度。本质上在二分的过程中,mid
并没有完全覆盖整个数组,这怎么理解呢?
r
值会一直缩小直到 l = r = 0
,而在这个过程中 mid
不会取到 0
值就返回了,也就是说返回的 l = 0
值并没有通过性质的判断,无法确定是否满足我们所设的二分性质,因为这个性质的边界是可能存在于小于 0
上的。因此,保险起见,需要对输出的 l
再进行一次判断即可。总结来说,Y总的模板一是左开右闭的。l
值会一直扩大直到 l = r = n - 1
,而在这个过程中 mid
不会取到 n - 1
值就返回了,也就是说返回的 l = n - 1
值并没有通过性质的判断,无法确定是否满足我们所设的二分性质,因为这个性质的边界是可能存在于大于 n - 1
上的。因此,保险起见,需要对输出的 l
再进行一次判断即可。总结来说,Y总的模板二是左闭右开的。l
是否落到范围边界上了,此时就需要再次判定一下。或者将范围扩大到 [-1, n - 1]
(模板一)/ [0, n]
(模板二)。l - r = 1
,按上述方法去分析的话,我们会发现模板三的 mid
会覆盖整个数组元素,因此模板三是左闭右闭的,且由于更新操作为r = mid - 1
或者l = mid + 1
,因此计算mid
时不需要加1
,mid = l + r >> 1
即可。