使用二分查找来判断一个有序序列中是否存在特定元素

你们玩过猜数字的游戏吗?你的朋友心里想一个 \(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\)。序列已经按照升序排好,保证了二分的有序性。

每一次二分,我们这样来做:

  1. 取区间中间值 \(mid = \lfloor \frac{L+R}{2} \rfloor\)
  2. 如果 \(a[mid] = x\),则说明找到了 \(x\),输出 \(x\) 对应的位置 \(mid\) 并结束查找;
  3. 如果 \(a[mid] \gt x\),由于序列是升序排列,所以区间 \([mid,R]\) 都可以排除,修改右边界 \(R = mid-1\)
  4. 如果 \(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;
}
posted @ 2020-04-16 15:43  quanjun  阅读(674)  评论(0编辑  收藏  举报