目录
【基本思路】
我们设定一个初始的L和R,保证答案在[L,R]
中,当[L,R]
中不止有一个数字的时候,取区间的中点M,询问这个中点和答案的关系,来判断答案是M,还是位于[L,M-1]
中,还是位于[M+1,R]
中。
【可视化】
提供一个二分查找的可视化图
Binary and Linear Search Visualization (usfca.edu)https://www.cs.usfca.edu/~galles/visualization/Search.html
【算法实现】
从左向右找到第一个:
//把R不断的向左推 int L = 0; int R = n - 1; while (L < R) { int mid = L + (R - L) / 2;//这里要向下取整 if (a[mid] < x) L = mid + 1; else R = mid; }
从右向左找到第一个:
//把L不断的向右推 int L = 0; int R = n - 1; while (L < R) { int mid = L + (R - L+1) / 2;//这里要向上取整 if (a[mid] < x) L = mid ; else R = mid-1; }
【时间复杂度】
每一次查找都会使区间的长度变为原来的一半,时间复杂度为:O(log n)
【简单题目示例】
给出一个从小到大排列好的数组,要找到第一个大于等于 x 的数字并输出。
输入n,x,以及数组a(已经从小到大排好序)
输入示例:
9 4 2 3 3 3 3 4 4 4 4
输出示例:
5
采用代码如下:
#include<iostream> using namespace std; #define N 100000 int a[N], n, x; int main() { cin >> n >> x; for (int i = 0; i < n; i++) cin >> a[i]; if (x > a[n - 1]) cout << "fail" << endl; int L = 0; int R = n - 1; while (L < R) { int mid = L + (R - L) / 2; if (a[mid] < x) L = mid + 1; else R = mid; } if (a[L] == x) cout << L +1<< endl;//这里的L+1是考虑数组下标从0开始 else cout << "fail" << endl; }
【应用范围】
(1)要求数组是有序的;
(2)其他有序结构:
【日期】将日期转化为YYYYMMDD的形式,利用(YYYYMMDD1+YYYYMMDD2)÷2 来找到两个日期的中点,如果遇到有月份不合法的日期,只需要向下取整到12月即可。
【字符串】由于字符串具有天然的字典序,因此可以给一组字符串进行字典序排序。string类中的比较函数sort()为给字符串进行字典序排序。
【二维数据】先比较第一维,第一维相同时再比较第二维。确定第一维相同时的区间,即找到第一个大于等于第二维最小值的指针,最后一个小于等于第二维最大值的指针,两个指针相减即确定了区间。
【统计x出现的次数】
【题目描述】
给出一个正整数 n,和一个长度为 n 的整数数组 a,再给出一个正整数 q,接下来给出 q个询问,每个询问包含一个整数 x,你需要输出 x 在数组 a 中出现了几次。
【基本思路】
利用二分查找从左向右和从右向左分别找到第一个大于等于 x 的指针和最后一个大于等于 x 的指针。新学到了两个函数,因为二分查找实现起来没有那么容易,所以在C++的STL库中提供了两个函数,可以分别实现上述功能,以下两个函数的实现都是依赖于二分查找。
lower_bound(数组头指针,数组尾指针,待查找字符 x)——>返回第一个大于等于 x 的指针
upper_bound(数组头指针,数组尾指针,待查找字符 x)——>返回最后一个大于等于 x 的指针
#include<algorithm>//头文件位置
有关于两个函数的更详细介绍放在下面
#include<iostream> #include<algorithm> #define N 100000 using namespace std; int n, q, x, a[N]; int main() { cin >> n; for (int i = 0; i < n; i++) cin >> a[i]; sort(a, a + n);//从小到大排序 cin >> q; while (q--) { cin >> x; int *lp = lower_bound(a, a + n, x); int *rp = upper_bound(a, a + n, x); cout << int(rp - lp) << endl;//得到区间长度 } return 0; }
【求方程的解】
【题目描述】
请输出方程 + 16 = 0的解,已知这个解在[-,]之间,并且函数f(x) = + 16在定义域上单调递增。输出的答案保留5位小数。
【注意】
(1)对于方程的解,变量类型应该设置为double型;
(2)代码段的最后两行提供了两种输出方式,一种是C语言版本,一种是C++版本;
(3)注意中点mid的求法,这里的区间不是数组下标,可以直接用(R + L) / 2。
#include<iostream> #include<iomanip> using namespace std; double f(double x) { return x * x * x + 16; } int main() { double L = -1e9, R = 1e9; while (R - L >= 1e-6) { double mid = (R + L) / 2; //注意这里中点的求法 if (f(mid) > 0) R = mid; else L = mid; } printf("%.5lf\n", L); cout << fixed<<setprecision(5)<<L << endl; }
【精度问题的讨论】如果我们将精度提高到小数点后15位呢?
尝试改写上述代码:
while (R - L >= 1e-16) printf("%.15lf\n", L); cout << fixed<<setprecision(15)<<L << endl;
得到的运行结果如下:
我们可以看到,并没有运行结果,说明程序陷入了死循环,那么原因是什么呢?为什么精度要求变高之后,会得不到输出结果呢?
这是因为double本身存在不小的精度误差,当我们通过R - L这样的方式来控制二分的终止条件,会出现很大的精度问题。
【解决办法】
可以采用固定次数二分的方法:
for (int i = 1; i <= 100; i++) { double mid = (R + L) / 2; //注意这里中点的求法 if (f(mid) > 0) R = mid; else L = mid; }
【注意】大约是,二分的初始条件是,最后足以把输出结果控制在左右。
得到结果如下:
【查找x第一次出现的位置】
可以利用lower_bound ()这个函数,因为该函数返回的是一个地址,那么用得到的地址减去头指针即为 x 的位置。
int pos = lower_bound(a + 1, a + n + 1, x) - a;