最全!二分查找, binary_search, lower_bound, upper_bound完全、严格、数学讲解(C/Python)

怎么查找

要在无序列表 a r r arr arr 中要查找 x x x ,只能遍历该列表,因为无序列表没有任何特征。

如果有排好序的列表呢?(下面文章中假设列表从小到大排序)

随便拿出某个元素。

  • 如果值小于 x x x ,那么 x x x 必须在其右侧,左侧的元素就可以放弃查找。
  • 如果值大于 x x x ,那么 x x x 必须在其左侧,右侧的元素也可以放弃查找。

那么取哪一个元素来筛选更好?(喊二分的同学先别急)

假设 x x x 出现在每个位置的概率都相等,那么

查看位于从左到右 p % p\% p% 位置的元素,

p % p\% p% 的概率 x x x 在左侧从而可以抛弃右侧 1 − p 1-p 1p 部分的元素;
1 − p 1 - p 1p 的概率 x x x 在右侧从而可以抛弃左侧 p p p 部分的元素。

数学来说,每次平均将抛弃 2 p ( 1 − p ) 2 p(1-p) 2p(1p) 比例的元素。根据一点数学知识, p = 0.5 p=0.5 p=0.5 时平均抛弃元素是最多的。

那么我们得出了最快的的策略:取中间位置的元素比较,大于x则下次在左侧继续取中找,小于x则在右侧继续找;

试写

先来个伪代码吧。

首先划定查找范围 [ l , r ] [l, r] [l,r] 。(下面记p为返回的位置p=search(...),a为返回的元素arr[p]

在每次查找中:

  1. 首先记录中间位置m = (l + r) / 2,整形除法自动向下取整
  2. 查看 arr[m]
  3. 如果 arr[m] > x,则 r = m-1; 继续找左侧
  4. 如果 arr[m] < x,则 l = m+1; 继续找右侧
  5. arr[m] == x,正好是要找的,立刻返回。隐患1

注意这里要加减1,否则 lr差1时,m始终等于l,若 arr[m] < x ,之后 l = m 没变化,就卡死在这里了。

加减1后,最终l, r肯定会重叠——用一个 while(l < r) 的循环包裹上面的步骤。

写出最简单的代码:

int binary_search(const int arr[], int size, int x) {
    int l = 0;
    int r = size - 1;
    int m = 0;
    while (l < r) { // 重叠就结束了 
        m = l + (r - l) / 2;
        if (arr[m] < x) {
            l = m + 1;
        }
        else if (arr[m] > x) {
            r = m - 1;
        } else{ // 最后比较相等,相等的情况最少
            return m;
        }
    }
    return m;
}
from typing import List, TypeVar

T = TypeVar('T')


def binary_search(arr: List[T], x: T):
    l = 0
    r = len(l) - 1
    while l < r:
        m = (l + r) // 2
        if arr[m] < x:
            l = m + 1
        elif arr[m] > x:
            r = m - 1
        else:
            return m
    return m

注意如果列表中没有 x,重叠时将会找到某个 a,它左侧元素都小于 x,右侧大于 x。(x本身大于还是小于不确定-隐患2

问题改进

上面提到了两个问题,都是关于查找目标x的:

  1. 如果列表中有多个x,找到的x可能是它们中的任一个。
  2. 如果列表中没有x,得到的arr[p]x关系不稳定。

有多个x

如果有连续多个x,找到的x可能是它们中的任一个。

问题1在只要求“找到”的情况下不是什么大问题。
但是如果要处理所有“找到”的元素,结果是最左或最右的x相对会更好。

  • 方便用单个循环处理。
  • 满足某些“首次出现”等算法题的要求

问题出在这里

  1. arr[m] == x,正好是要找的,立刻返回。

如果不马上返回,此时 arr[l] < xarr[r] > x

在这个查找范围里继续找总是能找到开始或末尾的x

假如我们想找末尾的x,一个自然的想法就是:

既然 arr[m] < x 时向右,那 arr[m] == x 时也继续向右不就行了?

简单设想现在查找范围里的元素是

x, x, x, m, ?, ?, ?
arr[r] > x, r向左移一格,
之后范围里都是x,l不断向右,最后l r重合,得到末尾的x

看来在 arr[m] <= x 时继续向右,我们总是找到末尾的x,成功!

同理,在arr[m] >= x时继续向左,总是找到开头的x。(同理)

没有x

如果列表中没有x,得到的arr[p]与x大小关系不确定。

问题2可以通过判断arr[p]的大小然后左右移动来解决,反正就一个if的事嘛。

但是在存储寸土寸金的时代,人们追求的是使用尽可能少的代码完成功能。

于是他们思考:

既然步骤2/3中我们比较了arr[m]和x,那么为什么不在l r重叠时再让循环比较一次呢?


将循环条件改成 while(l <= r)

没x且l r重叠时,元素们是:

(小于x),(不确定的a (l r 在这)),(大于x)

在下一次重复循环中:

  1. arr[m]<x
    此时 l右移了!
    我们将状态确定为了

    (小于x),(小于x (r在这)),(大于x (l移到这了!)

  2. arr[m]>x
    此时 r左移了!
    我们将状态确定为了

    (小于x (r移到这了!)),(大于x (l在这里)),(大于x)

非常完美呢!

现在arr[l]是第一个大于x的元素,arr[r]是小于x的最后一个元素,挑其中一个返回即可实现对应功能。

这里重复循环看似优雅,其实额外增加了一个问题:如果待查找在arr[0],那么r = mid - 1之后会变成-1,用unsigned就会溢出跑飞。

用int的话返回-1可以视为“没有比它小的了”的报错。

视情况使用咯。

综合考虑

综合上两部分:

1是改变了等于策略,肯定不会影响2无x时的结果。

无x时arr[l]是第一个大于x的元素,arr[r]是小于x的最后一个元素


那么,考虑一下2多循环一次会不会影响多x时的结果。

还是考察l r重叠时的最后一次循环

  1. 采取arr[m]<=x向右,重叠时找到的见上

    (等于x),(等于x (l r在这)),(大于x)

    然后 arr[m]<=xl右移
    变为

    (小于等于x),(小于等于x (r在这)),(大于x (l移到这了!)

    此时arr[l]是第一个大于x的元素,arr[r]等于x的最后一个元素,
    综合无x时,arr[l]是第一个大于x的元素,arr[r]小于等于x的最后一个元素

  2. 采取arr[m]<x向右(即arr[m]>=x向左),重叠时找到的见上

    (小于x),(等于x (l r在这)),(等于x)

    然后 arr[m]>=xr左移
    变为

    (小于x (r移到这了!)),(等于x (l在这)),(等于x)

    此时arr[l]是第一个等于x的元素,arr[r]是小于x的最后一个元素,
    综合无x时,arr[l]是第一个大于等于x的元素,arr[r]是小于x的最后一个元素

总结

p = search(l, x);
a = l[p]
挪l时arr[m]<xarr[m]<=x
返回la>=x(即C++ STL lower_bounda>x(即C++ STL upper_bound
返回ra<xa<=x

最终代码!

int binary_search(const int arr[], unsigned size, int x) {
    int l = 0;
    int r = size - 1;
    while (l <= r) {
        int m = l + (r - l) / 2; // 防溢出
        if (arr[m] <= x) l = m + 1; ;;;;;;/// 可改成小于,之后挪r时即arr[m]>=x ------- !
        else r = m - 1;
    }
    return l;;;;; /// 可改成r ------- !
}
from typing import List, TypeVar

T = TypeVar('T')


def binary_search(arr: List[T], x: T):
    l = 0
    r = len(l) - 1
    while l <= r: # 可改成小于,之后挪r时即arr[m]>=x ------- !
        m = (l + r) // 2
        if arr[m] <= x:
            l = m + 1
        else:
            r = m - 1
    return l # 可改成r ------- !
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值