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

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

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

  1. 取区间中间值 (mid = lfloor frac{L+R}{2} floor)
  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;
}
原文地址:https://www.cnblogs.com/quanjun/p/12713316.html