还不会二分查找?看这一篇就够了

15 篇文章 76 订阅

一、整数二分

二分查找分为整数二分和浮点数二分,一般所说的二分查找都是指整数二分。

1.1 二分查找模板

满足单调性的数组一定可以使用二分查找,但可以使用二分查找的数组不一定需要满足单调性。

不妨假设我们找到了条件 C 1 C_1 C1,它和它的对立条件 C 2 C_2 C2 能够将数组 a a a 一分为二,如下图所示:

因为 C 1 C_1 C1 C 2 C_2 C2 互为对立,故总有 C 1 ∪ C 2 ≡ True ,    C 1 ∩ C 2 ≡ False C_1\cup C_2\equiv \text{True},\;C_1\cap C_2\equiv \text{False} C1C2True,C1C2False(用C++语言描述,就是 c1 || !c1 总是为真,c1 && !c1 总是为假)。换句话说, ∀   x ∈ a \forall \,x\in a xa x x x 至少满足 C 1 C_1 C1 C 2 C_2 C2 中的一个,且 x x x 不会同时满足 C 1 C_1 C1 C 2 C_2 C2

观察上图可以发现,索引 3 3 3 和索引 4 4 4 这两个位置都可以作为 C 1 C_1 C1 C 2 C_2 C2 的分界点。其中,索引 3 3 3 是红色区域的右边界,索引 4 4 4 是绿色区域的左边界。而我们接下来要讨论的二分查找模板就是用来寻找 C 1 C_1 C1 C 2 C_2 C2 的分界点的

1.1.1 寻找右边界的二分查找

前面说过, C 1 C_1 C1 C 2 C_2 C2 的分界点一共有两个,因此我们的整数二分查找模板也有两个。一个用来查找右边界(即左侧的分界点,对应索引 3 3 3),一个用来查找左边界(即右侧的分界点,对应索引 4 4 4)。这里首先介绍查找右边界的模板。

因为查找的是红色区域的右边界,所以先定义一个函数 check(i),其中参数 i i i 是索引。当 i i i 位于红色区域,即 0 ≤ i ≤ 3 0\leq i \leq 3 0i3 时,check(i) 为真;当 i i i 位于绿色区域,即 4 ≤ i ≤ 7 4\leq i \leq 7 4i7 时,check(i) 为假。

初始时设置左右两个指针分别位于数组的左右两端,每次循环时计算 m i d = l + r 2 mid=\frac{l+r}{2} mid=2l+r(至于 m i d mid mid 到底取多少稍后会说),然后判断 check(mid) 的值(为实现二分查找,我们需要确保每次缩小区间时答案都落在区间内。这样一来,当最终 l == r 时,l 就是我们需要的答案)。如果 check(mid) 为真,说明 m i d mid mid 位于红色区域,且 m i d mid mid 有可能就是右边界,因此接下来令 l = m i d l=mid l=mid 来缩小查找范围(因为我们要保证缩小后的区间仍然包含答案);如果 check(mid) 为假,说明 m i d mid mid 位于绿色区域,且 m i d mid mid 必不可能是红色区域的右边界,因为 m i d mid mid 最多是索引 4 4 4,因此令 r = m i d − 1 r=mid-1 r=mid1 来缩小查找范围。

接下来重点关注 m i d mid mid 到底该取多少。如果 m i d = l + r 2 mid=\frac{l+r}{2} mid=2l+r,其中的除法代表整除,在某一轮循环出现了 r − l = 1 r-l=1 rl=1,则 m i d = 2 l + 1 2 = l mid=\frac{2l+1}{2}=l mid=22l+1=l。若 check(mid) 为真,则更新后的区间仍然为 [ l , r ] [l,r] [l,r],这就会导致无限循环。事实上,只需要取 m i d = l + r + 1 2 mid=\frac{l+r+1}{2} mid=2l+r+1,若 check(mid) 为真,则 m i d = r mid=r mid=r,更新后的区间为 [ r , r ] [r,r] [r,r],循环结束。若 check(mid) 为假,则更新后的区间为 [ l , l ] [l,l] [l,l],循环结束。

寻找右边界的二分查找模板:

int right_bound(int l, int r) {
    while (l < r) {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

1.1.2 寻找左边界的二分查找

类似1.1.1节中的分析,因为查找的是绿色区域的左边界,所以先定义一个函数 check(i),其中参数 i i i 是索引。当 i i i 位于绿色区域,即 4 ≤ i ≤ 7 4\leq i \leq 7 4i7 时,check(i) 为真;当 i i i 位于红色区域,即 0 ≤ i ≤ 3 0\leq i \leq 3 0i3 时,check(i) 为假。

初始时设置左右两个指针分别位于数组的左右两端,每次循环时计算 m i d = l + r 2 mid=\frac{l+r}{2} mid=2l+r(至于 m i d mid mid 到底取多少稍后会说),然后判断 check(mid) 的值。如果 check(mid) 为真,说明 m i d mid mid 位于绿色区域,且 m i d mid mid 有可能就是左边界,因此接下来令 r = m i d r=mid r=mid 来缩小查找范围;如果 check(mid) 为假,说明 m i d mid mid 位于红色区域,且 m i d mid mid 必不可能是绿色区域的左边界,因为 m i d mid mid 最多是索引 3 3 3,因此令 l = m i d + 1 l=mid+1 l=mid+1 来缩小查找范围。

接下来重点关注 m i d mid mid 到底该取多少。如果 m i d = l + r 2 mid=\frac{l+r}{2} mid=2l+r,其中的除法代表整除,在某一轮循环出现了 r − l = 1 r-l=1 rl=1,则 m i d = 2 l + 1 2 = l mid=\frac{2l+1}{2}=l mid=22l+1=l。若 check(mid) 为真,则更新后的区间为 [ l , l ] [l,l] [l,l],循环结束。若 check(mid) 为假,则更新后的区间为 [ r , r ] [r,r] [r,r],循环结束。综上所述, m i d mid mid l + r 2 \frac{l+r}{2} 2l+r 即可。

寻找左边界的二分查找模板:

int left_bound(int l, int r) {
    while (l < r) {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    return l;
}

1.2 应用:寻找元素的起始位置和终止位置

原题链接:AcWing 789. 数的范围

a = [1, 3, 3, 3, 4] 为例,数字 3 3 3 的起始位置和终止位置分别是 1 1 1 3 3 3(索引)。如何利用1.1节中的模板来完成此题呢?

现对数组 a a a 作出划分,若令 C 1 : a [ i ] < x ,    C 2 : a [ i ] ≥ x C_1:a[i]<x,\; C_2:a[i]\geq x C1:a[i]<x,C2:a[i]x(注意必须保证 C 1 , C 2 C_1,C_2 C1,C2 互为对立),则求 x x x 的起始位置便转化成了求 C 2 C_2 C2 区域的左边界。若令 C 1 : a [ i ] ≤ x ,    C 2 : a [ i ] > x C_1:a[i]\leq x,\; C_2:a[i]> x C1:a[i]x,C2:a[i]>x,则求 x x x 的终止位置便转化成了求 C 1 C_1 C1 区域的右边界。

在求 C 2 C_2 C2 区域的左边界时,check(mid) 即为 a[mid] >= x。在求 C 1 C_1 C1 区域的右边界时,check(mid) 即为 a[mid] <= x

AC代码如下:

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int a[N];

int left_bound(int l, int r, int x) {
    while (l < r) {
        int mid = l + r >> 1;
        if (a[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return l;
}

int right_bound(int l, int r, int x) {
    while (l < r) {
        int mid = l + r + 1 >> 1;
        if (a[mid] <= x) l = mid;
        else r = mid - 1;
    }
    return l;
}

int main() {
    int n, q;
    cin >> n >> q;
    for (int i = 0; i < n; i++) cin >> a[i];

    while (q--) {
        int k;
        cin >> k;
        int begin = left_bound(0, n - 1, k);
        if (a[begin] != k) cout << "-1 -1" << endl;
        else cout << begin << " " << right_bound(0, n - 1, k) << endl;
    }

    return 0;
}

二、浮点数二分

2.1 浮点数二分模板

相比整数二分,浮点数二分就要简单许多了,因为浮点数二分不涉及到边界问题。

浮点数二分通常用来求某个数 x x x 的近似值 x x x 不易直接求得,例如 x = 2 x=\sqrt{2} x=2 等)。由于此时左右两个指针也均为浮点数,所以我们不能直接判断 l == r,而是判断 r - l 是否小于预先设定的精度。

模板如下:

double fbsearch(double l, double r, double eps) {
    while (r - l > eps) {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

一些注意事项:

  • 若要求精确到小数点后 k k k 位,则 e p s eps eps 1 0 − ( k + 2 ) 10^{-(k+2)} 10(k+2);
  • m i d ≥ x mid\geq x midx,则 check(mid) 为真,否则为假。

2.2 应用:数的三次方根

原题链接:AcWing 790. 数的三次方根

注意到当 ∣ x ∣ < 1 |x|<1 x<1 时,有 ∣ x 1 / 3 ∣ > ∣ x ∣ |x^{1/3}|>|x| x1/3>x,故选取 l , r l,r l,r 时一定要谨慎。

因为 1000 0 1 / 3 < 22 10000^{1/3}<22 100001/3<22,所以这里取 l = − 22 ,   r = 22 l=-22,\,r=22 l=22,r=22 即可。

#include <iostream>

using namespace std;

double x;

double fbsearch(double l, double r, double eps) {
    while (r - l > eps) {
        double mid = (l + r) / 2;
        if (mid * mid * mid >= x) r = mid;
        else l = mid;
    }
    return l;
}

int main() {
    cin >> x;
    printf("%lf", fbsearch(-22, 22, 1e-8));
    return 0;
}

三、使用STL进行二分查找

3.1 std::binary_search

template< class ForwardIt, class T >
bool binary_search( ForwardIt first, ForwardIt last, const T& value );

该函数用来检查 [first, last) 区间内(该区间已排序)是否有数字等于 value,如果有返回 true,否则返回 false

3.2 std::lower_bound

template< class ForwardIt, class T >
ForwardIt lower_bound( ForwardIt first, ForwardIt last, const T& value );

该函数用来返回 [first, last) 区间内(该区间已排序)首个大于等于 value 的元素的迭代器,如果找不到这种元素则返回 last

3.3 std::upper_bound

template< class ForwardIt, class T >
ForwardIt upper_bound( ForwardIt first, ForwardIt last, const T& value );

该函数用来返回 [first, last) 区间内(该区间已排序)首个大于 value 的元素的迭代器,如果找不到这种元素则返回 last

3.4 std::equal_range

template< class ForwardIt, class T >
std::pair<ForwardIt, ForwardIt>
    equal_range( ForwardIt first, ForwardIt last, const T& value );

该函数用来返回 [first, last) 区间内(该区间已排序)所有等于 value 的元素的「范围」。

「范围」实际上是由两个迭代器构成的 pairpair 中的第一个元素是 std::lower_bound 的返回值,pair 中的第二个元素是 std::upper_bound 的返回值。


接下来我们用STL来简化1.2节中的代码。

注意到对于数组而言,其迭代器就是指针,因此我们可以通过将返回的迭代器与数组名作差来得到迭代器所指向的元素的下标。

简化后的代码如下:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e5 + 10;

int a[N];

int main() {
    int n, q;
    cin >> n >> q;
    for (int i = 0; i < n; i++) cin >> a[i];

    while (q--) {
        int k;
        cin >> k;
        auto p = equal_range(a, a + n, k);
        if (*p.first != k) cout << "-1 -1" << endl;
        else cout << p.first - a << " " << p.second - a - 1 << endl;
    }

    return 0;
}

References

[1] https://www.acwing.com/activity/content/punch_the_clock/11/
[2] https://www.acwing.com/blog/content/31/
[3] https://zh.cppreference.com/w/cpp/algorithm/lower_bound

  • 81
    点赞
  • 213
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
计算机据结构是计算机科学中非常重要的一个概念,它对于程序员和软件开发人员来说至关重要。学习计算机据结构可以帮助我们更好地理解和处理据,提高程序的效率和性能。下面是一个关于计算机据结构学习的任务建议: 1. 学习基础知识:首先,你需要了解计算机据结构的基本概念和术语。例如,了解组、链表、栈、队列和树等常见的据结构及其特点。 2. 学习算法据结构和算法密切相关,学习据结构的同时也要学习与之对应的常用算法。例如,学习排序算法(如冒泡排序、快速排序)、查找算法(如二分查找)等。 3. 实践编程:理论知识只有通过实践才能真正掌握。尝试用编程语言实现各种据结构和算法,编写相关的代码并进行调试和测试。 4. 解决问题:挑战自己解决一些与据结构相关的问题。可以参与在线编程竞赛、解决实际应用中的问题或者尝试解答一些经典的算法问题。 5. 深入学习高级据结构:一旦掌握了基本的据结构,可以进一步学习一些高级的据结构,如图、堆、哈希表等。这些据结构在实际应用中有着广泛的应用。 6. 阅读相关书籍和教程:除了自学,还可以通过阅读经典的计算机科学书籍和在线教程来加深对据结构的理解和应用。 7. 参与开源项目:参与开源项目是一个很好的锻炼机会,可以与其他开发者一起合作,共同学习和改进据结构和算法的实现。 8. 持续学习和实践:据结构是一个广阔而深奥的领域,持续学习和实践是非常重要的。参与讨论、阅读最新的研究论文和技术博客,保持对新技术和新思想的敏感度。 总之,学习计算机据结构需要坚持不懈的学习和实践。通过理论知识的掌握和实际应用的经验积累,你将能够更好地应用据结构来解决实际问题,提高程序的效率和性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Iareges

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值