二分查找(binary)

问题引入

假如计算机随机生成了一个在[1,100] 之间的数字x,由你来猜,但是只给你 88 次机会,每次会提醒你猜的数比生成的数大还是小,你如何保证一定能在 88 次以内猜中?

策略1

从小往大依次猜1,2,3,…,最坏情况下,如果数字是100,你需要猜100次!

策略2

从大往小猜,情况如上,最坏情况是1,也需要猜100 次!

策略3

随机猜,更不好把握次数!

提示

我们考略这个问题的时候,不妨反过来想,如何能够有把握地淘汰尽可能多的数?如果每次我们都猜正中间的数字,则我们一定能淘汰掉一半!

策略4

对于当前剩下的区间[l,r],我们猜第mid=(l+r)/2 个数字

        ·1

        如果a[mid]<x,则淘汰掉[l,mid] 区间内所有的数,在[mid+1,r] 区间内继续猜;

        ·2

        如果a[mid]>x,则淘汰掉[mid,r] 区间内所有的数,在[l,mid−1] 区间内继续猜;

        ·3

        如果a[mid]==x,结束。

像策略4这种,我们称为“二分查找”!

大家需要注意,使用“二分查找”的前提是,数组是有序的,通常在使用前可能需要先排序

算法介绍

二分查找是一个基础的算法,也叫折半查找。

二分查找就是将查找的数值和子数组的中间值做比较:

如果被查找的键等于中间值,找到元素,算法结束;

如果被查找的键小于中间值,就在左子数组中继续查找;

如果被查找的键大于中间值,就在右子数组中继续查找;

所谓中间值,就是此次查找区间内位于正中间的数值,如区间为[l,r],则中间值的位置为 (l+r)/2。

例:输入一个 100 以内的整数 k,通过二分查找猜出 k 的值,看看用了几次。

#include <cstdio>
int main() {
    int k;
    scanf("%d", &k);
    int lo = 1, hi = 100, cnt = 0;
    while (lo <= hi) {
        int mid = (lo + hi) >> 1;
        printf("%d: %d\n", ++cnt, mid);
        if (mid == k) return 0;
        if (mid < k) lo = mid + 1;
        else hi = mid - 1;
    }
    return 0;
}

有时,我们并非二分查找某个具体的值,而是去查找最大数的最小值,或最小数的最大值,这时候,我们通常只有在二分查找结束的时候才能获取数值

例: 

#include <cstdio>
bool valid(int k) {
    //判断k是否是合法的,如果合法返回true,否则返回false
}
int main() {
    int k;
    scanf("%d", &k);
    int left = 1, right = 100, cnt = 0;
    while (left <= right) {
        int mid = (left + right) >> 1;
        //求最小数的最大值
        if (valid(mid)) left = mid + 1;
        else right = mid - 1;
    }
    printf("%d\n", right);
    return 0;
}
#include <cstdio>
bool valid(int k) {
    //判断k是否是合法的,如果合法返回true,否则返回false
}
int main() {
    int k;
    scanf("%d", &k);
    int left = 1, right = 100, cnt = 0;
    while (left <= right) {
        int mid = (left + right) >> 1;
        //求最大数的最小值
        if (valid(mid)) right = mid - 1;
        else left = mid + 1;
    }
    printf("%d\n", left);
    return 0;
}

简单地记作求最大数的最小值是l,求最小数的最大值是r

巧妙的方法

方法

《算法进阶指南》中给出了另一种颇为巧妙的方法:

//在单调递增序列 a 中查找 >=x 的数中最小的一个(即 x 或 x 的后继)
while (l < r) {
	int mid = (l + r) >> 1;
	if (a[mid] >= x) r = mid;
	else l = mid + 1;
}
return a[l];
//在单调递增序列 a 中查找 <=x 的数中最大的一个(即 x 或 x 的前驱)
while (l < r) {
	int mid = (l + r + 1) >> 1;
	if (a[mid] <= x) l = mid;
	else r = mid - 1;
}
return a[l];

不难发现:mid = (l+r) >> 1 不会取到 rmid = (l + r + 1) >> 1 不会取到 l

实际应用

我们可以利用这一性质来处理无解的情况,把最初的二分区间 [1,n] 分别扩大为 [1,n+1] 和 [0,n],把a 数组的一个越界的下标包含进来。如果最后二分终止于扩大后的这个越界下标上,则说明a中不存在所求的数。

总结

总而言之,正确写出这种二分的流程是:

  1. 通过分析具体问题,确定左右半段哪一个是可行区间,以及 mid 归属哪一半段;

  2. 选择 mid = (l + r) >> 1, r = mid, l = mid + 1 和 mid = (l + r + 1) >> 1, r = mid - 1, l = mid 两个配套形式之一;

  3. 二分终止条件是 l == r,该值就是答案所在位置

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值