你们玩过猜数字的游戏吗?你的朋友心里想一个 (1000) 以内的正整数,你可以给出一个数字 (x),你朋友只要回答 “比 (x) 大” 或者 “比 (x) 小” 或者 “猜中”,你能保证在 (10) 次以内猜中吗?运气好只要一次就能猜中。
开始猜测是 (1) 到 (1000) 之间,你可以先猜 (500) ,运气好可以一次猜中;如果答案比 (500) 大,显然答案不可能在 (1) 到 (500) 之间,下一次猜测的区间变为 (501) 到 (1000);如果答案比 (500) 小,那答案不可能在 (500) 到 (1000) 之间,下一次猜测的区间变为 (1) 到 (499)。只要每次都猜测区间的中间点,这样就可以把猜测区间缩小一半。由于 (frac{1000}{2^{10}} lt 1),因此不超过 (10) 次询问区间就一个缩小为 (1),答案就会猜中了,这就是 二分查找 的基本思想 ——
每一次使得可选范围缩小一半,最终使得范围缩小为一个数,从而得出答案。
假设问的范围是 (1) 到 (n),根据 (frac{n}{2^x} le 1) 得 (x ge ext{log}_2n),所以我们只需要问 (sim log_2n) 次就可以查找到元素的位置(或者得到元素不存在)。
需要注意的是使用二分查找有一个重要的前提,就是 有序性 。接下来通过一个例子来入门二分查找的应用。
例1 找数。
题目描述:
给一个长度为 (n) 的单调递增的正整数序列,即序列中每一个数都比前一个数大。有 (m) 个询问,每次询问一个 (x) ,问序列中最后一个小于等于 (x) 的数是什么?
输入:
第一行两个整数 (n,m(1 le n,m le 1000))。
第二行 (n) 个整数用于表示这个序列。
接下来 (m) 行每行包含一个整数,用于表示询问的 (x)。
输出:
输出共 (m) 行,对于每一次询问的 (x) ,输出序列中值为 (x) 的元素的位置;如果序列中不存在置为 (x) 的元素,输出单独的一行 (-1)。
样例输入:
5 3
1 3 5 7 9
3
4
5
样例输出:
2
-1
3
问题分析
我们用 (L) 表示询问的区间的左边界,用 (R) 表示询问的区间的右边界, ([L,R]) 组成询问区间。
一开始 (L=1,R=n)。序列已经按照升序排好,保证了二分的有序性。
每一次二分,我们这样来做:
- 取区间中间值 (mid = lfloor frac{L+R}{2} floor);
- 如果 (a[mid] = x),则说明找到了 (x),输出 (x) 对应的位置 (mid) 并结束查找;
- 如果 (a[mid] gt x),由于序列是升序排列,所以区间 ([mid,R]) 都可以排除,修改右边界 (R = mid-1);
- 如果 (a[mid] lt x),由于序列是升序排列,所以区间 ([L,mid]) 都可以排除,修改左边界 (L = mid+1)。
重复执行二分直到 (L gt R)(因为最终循环结束时一定是 (L = R+1))。
示例代码如下:
#include <bits/stdc++.h>
using namespace std;
int n, m, a[1001], x;
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i ++) cin >> a[i];
while (m --) {
cin >> x;
int L = 1, R = n, res = -1;
while (L <= R) {
int mid = (L + R)/2;
if (a[mid] == x) {
res = mid;
break;
}
else if (a[mid] > x) R = mid-1;
else L = mid+1;
}
cout << res << endl;
}
return 0;
}