使用二分查找来判断一个有序序列中是否存在特定元素
你们玩过猜数字的游戏吗?你的朋友心里想一个 \(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 \text{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} \rfloor\);
- 如果 \(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;
}