二分搜索问题有两种典型的形态:
- 找特定值的位置。
- 找第一个满足条件的值的位置。
但是每次解决这种问题时,都要在边界判断上试错,耗费大量时间。所以就借机记录下模板,以后直接套用。
一、寻找特定值的二分
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_first
与bs_last
也是一种选择。
写在最后:
- 代码已经过测试。你可以在这里找到源码和测试文件:戳这里
- 本人初来乍到,如有谬误欢迎雅正。