二分查找 | C++

以此题为例:P2249 【深基13.例1】查找


二分查找

对于一个单调不降的序列 \(S\),传统查找的复杂度是 \(\mathcal O(|S|)\),即 \(\mathcal O(n)\). 有时候序列 \(S\) 中的元素特别多,或者你希望尽量减小复杂度,那么,有没有复杂度更低的方法呢? 理论上是不行的,因为读入的复杂度已经达到O(n),而且大多数时候我们还需要 O(n·logn) 的复杂度对序列排序

然而实际上(比如例题),有时候我们需要查找很多次,读入时复杂度是 \(\mathcal O(n + m)\),然而查找时复杂度就成了 \(\mathcal O(nm)\),这时就有可能超时。是否有其他的算法可以降低复杂度呢?

当然是有的。我们可以使用 STL 里的 lower_boundupper_bound. 还记得你是怎么在英语词典里查单词的吗?字典中的单词是按照“字典序”进行排序的(类似例题中的单调不降)。如果我们要找一个单词,就要将字典从中间翻开,然后将这面单词跟想要找的单词比较。如果这面单词在需要寻找的单词之前,就将字典往后翻,否则就往前翻,直到找到准确的单词为止。(这句话与推广部分第一句皆引用自洛谷)

我们可以使用类似的方式进行查找,即二分查找。二分查找时,先判断序列 \(S\) 最中间的元素 \(S_{\frac{|S|}{2}}\) 与查找的元素之间的大小关系,由于序列是单调不减的,因此我们可以依据刚刚判断得到的关系进一步对序列 \(S\) 一半的区间继续使用相同的方式查询,直到得出结果为止


二分查找的实现

只需要按我刚才讲解的进行模拟即可,注意该序列可能存在重复元素,如果待查找元素在该序列存在多个,千万不要使用 while 循环往前一个一个推,我们继续二分即可,否则复杂度会退化,极端情况可能退化至 \(\mathcal O(\frac {n \cdot m} {2})\) 仍然可能超时。

继续二分的策略的缺点是将复杂度锁死在 \(\mathcal O(m \cdot \log n)\),但这个方式已经将算法的时间复杂度拉得够低了,足够过例题了。具体代码如下:

#include <bits/stdc++.h>

using namespace std;

const int N = 1e6+5;

int a[N];

int n, m, x;

int find() {
    int mid, l = 1, r = n, ans = -1;
	scanf("%d", &x);
	while(l <= r) {
		mid = (l + r) / 2;
		if (x < a[mid]) r = mid - 1;
		else if (x > a[mid]) l = mid + 1;
		else {
			r = mid - 1;
			ans = mid;
		}
	}
    return ans;
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    for (int i = 1; i <= m; i++) printf("%d ", find());
    return 0;
}

使用递归实现二分

等一等,不停使用相同方式查询并缩小范围,有没有感觉很熟悉?没错,二分查找还可以使用递归实现,代码更加优美、易懂,更适合装B.

需要注意的是,递归复杂度的常数更大,可能会被卡常,所以最好不要装B.

具体代码如下:

#include <bits/stdc++.h>

const int N = 1e6 + 5;

int st[N];

int n, m;

int find(int ans, int num, int* arr, int l, int r) {
    if (l > r) return ans; int mid = l + (r - l) / 2;
    if (arr[mid] < num) return find(ans, num, arr, mid + 1, r);
    if (arr[mid] > num) return find(ans, num, arr, l, mid - 1);
    ans = mid; return find(ans, num, arr, l, mid - 1);
    return ans;
}

int main() {
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d", &st[i]);
    for (int i = 1, tmp; i <= m; i++) {
        scanf("%d", &tmp);
        printf("%d ", find(-1, tmp, st, 1, n));
    }
    return 0;
}

二分思想的推广

除了二分查找之外,二分思想还能求出可行解的最值问题,比如想知道某款手机最高能多少楼高度摔下来而不会摔坏,使用二分的方式可以用最小实验次数就能得到结果(当然你需要准备好几个样品),这种思想被称为二分答案。

二分查找与二分答案的思想也可以用于排序,只不过算法名称不再是二分,而是分治(类二分查找思想)与倍增(类二分答案思想),由于篇幅原因,这里就不展开讲了,感兴趣的读者可自行查阅资料

二分思想本身非常简单,大家只需要多练习,很快就可以掌握二分查找和二分答案。不说了,赶紧去刷题!

posted @ 2024-07-18 20:11  Bramble_Marshall  阅读(38)  评论(1编辑  收藏  举报