问题引入
假如计算机随机生成了一个在[1,100] 之间的数字x,由你来猜,但是只给你 88 次机会,每次会提醒你猜的数比生成的数大还是小,你如何保证一定能在 88 次以内猜中?
策略1
从小往大依次猜1,2,3,…,最坏情况下,如果数字是100,你需要猜100次!
策略2
从大往小猜,情况如上,最坏情况是1,也需要猜100 次!
策略3
随机猜,更不好把握次数!
提示
我们考略这个问题的时候,不妨反过来想,如何能够有把握地淘汰尽可能多的数?如果每次我们都猜正中间的数字,则我们一定能淘汰掉一半!
策略4
对于当前剩下的区间[l,r],我们猜第mid=(l+r)/2 个数字
·1
如果a[mid]<x,则淘汰掉[l,mid] 区间内所有的数,在[mid+1,r] 区间内继续猜;
·2
如果a[mid]>x,则淘汰掉[mid,r] 区间内所有的数,在[l,mid−1] 区间内继续猜;
·3
如果a[mid]==x,结束。
像策略4这种,我们称为“二分查找”!
大家需要注意,使用“二分查找”的前提是,数组是有序的,通常在使用前可能需要先排序。
算法介绍
二分查找是一个基础的算法,也叫折半查找。
二分查找就是将查找的数值和子数组的中间值做比较:
如果被查找的键等于中间值,找到元素,算法结束;
如果被查找的键小于中间值,就在左子数组中继续查找;
如果被查找的键大于中间值,就在右子数组中继续查找;
所谓中间值,就是此次查找区间内位于正中间的数值,如区间为[l,r],则中间值的位置为 (l+r)/2。
例:输入一个 100 以内的整数 k,通过二分查找猜出 k 的值,看看用了几次。
#include <cstdio>
int main() {
int k;
scanf("%d", &k);
int lo = 1, hi = 100, cnt = 0;
while (lo <= hi) {
int mid = (lo + hi) >> 1;
printf("%d: %d\n", ++cnt, mid);
if (mid == k) return 0;
if (mid < k) lo = mid + 1;
else hi = mid - 1;
}
return 0;
}
有时,我们并非二分查找某个具体的值,而是去查找最大数的最小值,或最小数的最大值,这时候,我们通常只有在二分查找结束的时候才能获取数值。
例:
#include <cstdio>
bool valid(int k) {
//判断k是否是合法的,如果合法返回true,否则返回false
}
int main() {
int k;
scanf("%d", &k);
int left = 1, right = 100, cnt = 0;
while (left <= right) {
int mid = (left + right) >> 1;
//求最小数的最大值
if (valid(mid)) left = mid + 1;
else right = mid - 1;
}
printf("%d\n", right);
return 0;
}
#include <cstdio>
bool valid(int k) {
//判断k是否是合法的,如果合法返回true,否则返回false
}
int main() {
int k;
scanf("%d", &k);
int left = 1, right = 100, cnt = 0;
while (left <= right) {
int mid = (left + right) >> 1;
//求最大数的最小值
if (valid(mid)) right = mid - 1;
else left = mid + 1;
}
printf("%d\n", left);
return 0;
}
简单地记作:求最大数的最小值是l,求最小数的最大值是r
巧妙的方法
方法
《算法进阶指南》中给出了另一种颇为巧妙的方法:
//在单调递增序列 a 中查找 >=x 的数中最小的一个(即 x 或 x 的后继)
while (l < r) {
int mid = (l + r) >> 1;
if (a[mid] >= x) r = mid;
else l = mid + 1;
}
return a[l];
//在单调递增序列 a 中查找 <=x 的数中最大的一个(即 x 或 x 的前驱)
while (l < r) {
int mid = (l + r + 1) >> 1;
if (a[mid] <= x) l = mid;
else r = mid - 1;
}
return a[l];
不难发现:mid = (l+r) >> 1
不会取到 r
;mid = (l + r + 1) >> 1
不会取到 l
。
实际应用
我们可以利用这一性质来处理无解的情况,把最初的二分区间 [1,n] 分别扩大为 [1,n+1] 和 [0,n],把a 数组的一个越界的下标包含进来。如果最后二分终止于扩大后的这个越界下标上,则说明a中不存在所求的数。
总结
总而言之,正确写出这种二分的流程是:
-
通过分析具体问题,确定左右半段哪一个是可行区间,以及
mid
归属哪一半段; -
选择
mid = (l + r) >> 1, r = mid, l = mid + 1
和mid = (l + r + 1) >> 1, r = mid - 1, l = mid
两个配套形式之一; -
二分终止条件是
l == r
,该值就是答案所在位置。