二分查找

本文将使用画图来理解二分,并且介绍了经典的二分查找板子,例题。

能够使用二分查找的两个前提:

  • 二分查找仅使用于数组,这样才能通过判断大小关系来排除一半的搜索区间;
  • 要求输入数据是有序的,而在链表中使用效率很低,因为其在循环中需要跳跃式(非连续地)访问元素。

整数二分

根据上面两个前提,很多人认为二分的本质是单调性,但实际上并不是的,二分的本质是边界

假设给定一个区间,在这个区间上给定了某种性质,使得在右半区间满足这个性质,左半区间不满足这个性质,那么二分既能够查找左半区间的边界,也可以寻找右半区间的边界。

image-20230118203344229

现在我们来看一下 左半区间二分查找:

image-20230118213519331

再看一下右半区间二分查找:

image-20230118214805016

那么这里有一个问题,

为什么两种二分,mid的写法有一个 + 1有一个并没有(对应向上取整和向下取整)

下面举一个例子:

看一下为什么要加上1

image-20230118221434740

整数二分实战思想

如果你想让答案在右边区间那么你得让mid满足左区间的性质,反之,可求左区间

整数二分实战模板

二分模板一共有两个,分别适用于不同情况。

版本1

其更新操作是r = mid,计算mid时不需要加1。

1
2
3
4
5
6
7
8
int binarySearch(int l, int r){
while(l < r){
int mid = l + r >> 1;
if(check(mid)) r = mid;
else l = mid + 1;
}
return l;
}

image-20230118224520428

版本2

其更新操作是l = mid,此时为了防止死循环,计算mid时需要加1。

1
2
3
4
5
6
7
int binarySearch(int l, int r){
while(l < r){
int mid = l + r + 1 >> 1;
if(check(mid)) l = mid;
else r = mid - 1;
}
}

image-20230118224158712

复杂度分析

时间复杂度:O(logn)(折半查找树)

空间复杂度:O(1)(使用常数大小空间 l, r, mid)

大数越界处理

当数组长度很大时,加法 (l + r) 的结果可能超出 int 类型的取值范围。可换为:

1
2
3
int mid = l + r >> 1;
// 换成
int mid = l + (r - l) >> 1;

整数二分经典例题

AcWing789.数的范围

问题描述

给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。

对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。

如果数组中不存在该元素,则返回 -1 -1

输入格式

第一行包含整数 n 和 q,表示数组长度和询问个数。

第二行包含 n 个整数(均在 1∼10000 范围内),表示完整数组。

接下来 q 行,每行包含一个整数 k,表示一个询问元素。

输出格式

共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。

如果数组中不存在该元素,则返回 -1 -1

数据范围

1 ≤ n ≤ 100000
1 ≤ q ≤ 10000
1 ≤ k ≤ 10000

输入样例:

1
2
3
4
5
6 3
1 2 2 3 3 4
3
4
5

输出样例:

1
2
3
3 4
5 5
-1 -1

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, Q; // 整数个数、询问个数
int q[N];

int main()
{

cin >> n >> Q;

for (int i = 0; i < n; i ++ ) cin >> q[i];

while(Q --){
// 需要查找的数
int x; cin >> x;

// 第一次二分
int l = 0 , r = n - 1;

// 先找左边开始,第一个出现的 x,那么满足右边性质即可
while(l < r){
int mid = l + r >> 1;
if(q[mid] >= x) r = mid;
else l = mid + 1;
}
// 如果没找到
if(q[l] != x) puts("-1 -1");
else{
// 如果找到了
cout << l << " ";
// 开始找右边开始,第一个出现的x,那么满足左边的性质即可
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;
}
//if(q[l] != x) puts("-1");
// else cout << l << endl;
// 就算从右边也一定会找到一个值,它可以是左边开始找,找到的那个x
cout << l << endl;
}
}
return 0;
}

浮点数二分经典例题

AcWing 790. 数的三次方根

问题描述

给定一个浮点数 n,求它的三次方根。

输入格式

共一行,包含一个浮点数 n 。

输出格式

共一行,包含一个浮点数,表示问题的解。

注意,结果保留 6 位小数。

数据范围

−10000 ≤ n ≤ 10000

输入样例:

1
1000.00

输出样例:

1
10.000000

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

int main()
{

double n;
cin >> n;
// 依据数据范围取[min, max]
double l = -10000 , r = 10000;
// 调整精度
while(r - l > 1e-8){
double mid = (l + r) / 2;
if(mid * mid * mid >= n) r = mid; // 重点
else l = mid; // 重点
}

printf("%lf\n" , l);

}

技术支持

hello-algo
AcWing789.数的范围
AcWing 790. 数的三次方根