【二分搜索】以不变应万变,用模板应对90%+的二分搜索问题

二分搜索问题有两种典型的形态:

  • 找特定值的位置。
  • 找第一个满足条件的值的位置。

但是每次解决这种问题时,都要在边界判断上试错,耗费大量时间。所以就借机记录下模板,以后直接套用。

一、寻找特定值的二分

def bs_base(nums, target):
    """
    从元素各不相同的nums中找到target所在的下标。
    如果没有找到,则返回-index-1,表示target应该插入index前。
    """
    lo, hi = 0, len(nums) - 1

    while lo <= hi:
        mi = (lo + hi) // 2
        if nums[mi] < target:
            lo = mi + 1
        elif nums[mi] > target:
            hi = mi - 1
        else:
            return mi

    return - lo - 1

这个比较简单。最后一行是对Miss情况的处理。

二、寻找首个满足条件元素的二分

其实,只要查询的对象具有“单调性”,那么这类问题就都可以用二分法解决。

栗子1:在有序数组中,寻找大于某个数的最小数。

栗子2:在有序、有重复数据的数组中,寻找某个数第一次出现的位置。

栗子 3:寻找两个数组的最长公共子数组。若两个数组有长为k的公共子数组,那么自然有长为1 ~ (k-1)的。所以这个问题可以二分解决。


模板只需要修改几个地方即可。边界的判断只需要判 l o lo lo超出上限。
若其中条件为nums[mi] > x,则计算的是大于x的第一个数出现的位置。

def bs_first(nums, condition):
    """
    从nums中找到符合条件的、最靠前的元素所在下标。
    如果没有任何一个元素符合条件,返回-1。
    这里的condition是一个函数引用。
    E.g. bs_first([0,1,2,3], lambda a: a > 1.5)的返回是2
    """
    lo, hi = 0, len(nums) - 1

    while lo <= hi:
        mi = (lo + hi) // 2
        if not condition(nums[mi]):
            lo = mi + 1
        else:
            hi = mi - 1

    # 此时lo == hi + 1
    if lo >= len(nums):
        return -1

    return lo

三、扩展

第二个模板可以应对很多二分问题。

1. 寻找最后一个满足条件元素的二分

按照模板,直接调换 l o lo lo h i hi hi的逻辑即可

def bs_last(nums, condition):
    """
    从nums中找到符合条件的、最靠后的元素所在下标。
    如果没有任何一个元素符合条件,返回-1。
    """
    lo, hi = 0, len(nums) - 1

    while lo <= hi:
        mi = (lo + hi) // 2
        if condition(nums[mi]):     # 翻转条件
            lo = mi + 1
        else:
            hi = mi - 1

    # 没有额外判断的原因是,Miss时hi本来就为-1

    return hi   # lo -> hi

2. 查找某个数字的起始位置

E.g. [1,1,2,2,2,4,4] 中查找第一个2。如果不存在返回-1。

直接套用bs_first,Miss的情况单独考虑即可。

def bs_first_num(nums, target):
    """
    从nums中找到第一个target出现位置的下标。
    如果target没有出现,返回-1。
    """
    lo, hi = 0, len(nums) - 1

    while lo <= hi:
        mi = (lo + hi) // 2
        if not nums[mi] >= target:
            lo = mi + 1
        else:
            hi = mi - 1

    # 仅在模板bs_first上改变这里即可
    if lo >= len(nums) or nums[lo] != target:
        return -1

    return lo

3. 查找某个数字的范围(即始末位置)

E.g. [1,1,2,2,2,4,4] 中查找2的范围,返回[2,4]
直接套用bs_first两次,自然就可以算出范围了。

def bs_range(nums, target):
    """
    从nums中找到target出现的区段
    如果target没有出现,返回[-1, -1]
    """
    lower = bs_first(nums, lambda a: a >= target)
    if lower == -1 or nums[lower] != target:
        return [-1, -1]
    upper = bs_first(nums, lambda a: a > target)
    return [lower, upper - 1 if upper != -1 else len(nums) - 1]

当然,用bs_firstbs_last也是一种选择。


写在最后:

  1. 代码已经过测试。你可以在这里找到源码和测试文件:戳这里
  2. 本人初来乍到,如有谬误欢迎雅正。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值