再也不担心用不好二分法了,因为我找到了"作弊"的接口

简介: 导读:算法是程序的灵魂,而复杂度则是算法的核心指标之一。为了降低复杂度量级,可谓是令无数程序员绞尽脑汁、甚至是摧枯秀发。一般而言,若能实现对数阶的时间复杂度,算法效率往往就已经非常理想。而实现对数阶的常用思想莫过于二分。二分常有,好用的二分并不常有。while条件是lo<hi还是lo<=hi?分支判断mid是+1还是-1还是仍然取值mid?最后return哪个值?如果目标序列不是严格递增又该怎么处理?想想都不禁让人敬而远之。幸运的是,在python语言中,已经内置了成熟的二分函数。

导读:算法是程序的灵魂,而复杂度则是算法的核心指标之一。为了降低复杂度量级,可谓是令无数程序员绞尽脑汁、甚至是摧枯秀发。一般而言,若能实现对数阶的时间复杂度,算法效率往往就已经非常理想。而实现对数阶的常用思想莫过于二分。


二分常有,好用的二分并不常有。while条件是lo<hi还是lo<=hi?分支判断mid是+1还是-1还是仍然取值mid?最后return哪个值?如果目标序列不是严格递增又该怎么处理?想想都不禁让人敬而远之。幸运的是,在python语言中,已经内置了成熟的二分函数。


640.jpg看完本文,二分不再是空中楼阁,而会"真香"


01 初识bisect

bisect.py是一个独立的模块文件,默认存放在安装目录下的Lib文件夹中(例如:..\Python\Python37\Lib),整个代码实现非常简单,仅仅是定义了4个函数(算上等价的是6个),共计不到100行。


主要实现了二分查找和二分插入两项大的功能,为了兼顾目标序列非严格递增的情形(即允许有重复值),查找和插入又都区分是左查找还是右查找、左插入还是右插入。

默认情况下是右查找和右插入。


02 方法示例

应用二分法主要有两种情形:一种是将目标元素插入到已知有序列表中,另一种是在有序列表中查找目标元素索引。


首先来看二分插入。bisect模块提供了两种插入方式,主要是考虑兼容目标列表不是严格递增的情况,其中:

  • insort_right:右插入,即将目标元素插入到尽可能靠右的位置,当在列表中遇到等值目标元素时,会继续向右探索,直至小于右边元素的位置才插入
  • insort_left:左插入,与右插入相反,即当存在重复元素时,尽可能靠左地插入至列表
  • insort:完全等价于insort_right
  • 除了列表和待插入元素外,还支持两个缺省参数lo和hi,其中lo默认为0,hi默认为列表长度,区间是左闭右开,即[lo, hi)
  • 当限定了目标区间后,则元素只在指定区间进行比较,并插入到最右或者最左端,而不管区间外元素的大小


看个示例会更加清楚:


from bisect import *
lyst = [1, 3, 3, 5, 7]
insort_left(lyst, 3)
print(lyst) #[1, 3, 3, 3, 5, 7]
insort_right(lyst, 4, 0, 3) #指定lo=0, hi=3
print(lyst) #[1, 3, 3, 4, 3, 5, 7],因为限定区间,所以插入了错误位置

对于纯数字列表而言,插入本无左右之分


再看返回索引的二分查找。提供了两种返回索引的原则,即最左索引还是最右索引:

  • bisect_right:返回尽可能靠右的插入目标元素索引,保证返回索引位置满足:右侧的元素都严格大于目标元素,左侧元素小于或者等于目标元素
  • bisect_left:与前者相反,会返回尽可能靠左的插入目标元素索引
  • bisect:完全等价于bisect_right
  • 特殊情况下,当目标元素比列表中所有元素都大时,返回len(lyst);反之,当目标元素比列表中所有元素都小时,返回索引0


from bisect import *
lyst = [1, 3, 3, 5, 7]
index = bisect_right(lyst, 3)
print(index) #index = 3,存在重复元素,靠右插入
index = bisect_right(lyst, 6, lo=0, hi=3)
print(index) #index = 3,限定插入区间后,仅在该区间比对,返回index有误
index = bisect_left(lyst, 3)
print(index) #index = 1,存在重复元素,靠左插入
index = bisect_left(lyst, 9)
print(index) #index = len(lyst) = 5 
index = bisect_left(lyst, 0)
print(index) #index = 0


03 改造重写

Python内置的二分法虽然好用,但不得不说功能还是太单一了。都是插入函数,它能用于原原本本的二分查找吗,带返回-1那种?不能。除了单列表类型,还能用于其他数据结构吗?好像也不行。但我们可以对其稍微进行改造,以实现更多个性化的二分函数。


  • 鉴于字符串具有比较操作,bisect天然支持字符串列表的插入和查找


from bisect import *
strs = ['ab', 'cd', 'ef', 'gh']
insort(strs, 'cd')
print(strs) # ['ab', 'cd', 'cd', 'ef', 'gh']
str_ = 'abcefg'#因字符串不可原地更改,仅支持查找索引而不能插入
index = bisect(str_, 'b')
print(index) # index = 2


  • 稍加改造,也能实现原原本本的二分查找


from bisect import *
lyst = [1, 3, 3, 5, 7]
tmp = bisect(lyst, 4)
index = tmp if tmp>0 and lyst[tmp-1] == 4 else -1
print(index) #查找元素不存在,返回index = -1
tmp = bisect(lyst, 3)
index = tmp if tmp>0 and lyst[tmp-1] == 3 else -1
print(index) #查找元素存在,返回靠右的元素索引index = 3
#bisect = bisect_right,所以与左侧值比较是否存在目标元素;若想返回靠左索引,可类似改造bisect_left


  • 参照内置函数,改写比较方法,可实现定制的二分法


"""内置bisect函数
def bisect_right(a, x, lo=0, hi=None):
    if lo < 0:
        raise ValueError('lo must be non-negative')
    if hi is None:
        hi = len(a)
    while lo < hi:
        mid = (lo+hi)//2
        if x < a[mid]: hi = mid
        else: lo = mid+1
    return lo
"""
def my_bisect_right(a, x, lo=0, hi=None):
    """
    x元素数据结构形如('A', 19)
    a是x类元素的列表,根据x中的数值变量排序
    函数计划返回靠右的插入索引
    """
    if lo < 0:
        raise ValueError('lo must be non-negative')
    if hi is None:
        hi = len(a)
    while lo < hi:
        mid = (lo+hi)//2
        if x[1] < a[mid][1]: hi = mid###仅改写此行比较方法即可实现定制
        else: lo = mid+1
    return lo
lyst = [('A', 19), ('B', 20), ('C', 20), ('D', 22)]
x = ('X', 20)
index = my_bisect_right(lyst, x)
print(index) #index = 3


04 效率测试按照bisect文档说明,内置二分法函数用C语言重写以实现效率上提高,我们原原本本copy一个内置函数的实现,用作效率对比。


from bisect import bisect_right
###直接copy内置函数实现的my_bisect_right,用作对比
def my_bisect_right(a, x, lo=0, hi=None):
    if lo < 0:
        raise ValueError('lo must be non-negative')
    if hi is None:
        hi = len(a)
    while lo < hi:
        mid = (lo+hi)//2
        if x < a[mid]: hi = mid
        else: lo = mid+1
    return lo
from time import time
lyst = list(range(10**6))#有序列表长度为100万
start = time()
for _ in range(10**6):#二分效率太高,所以重复100万次
    index = my_bisect_right(lyst, 100)
print('my_bisect_right time used: ', time()-start)
#my_bisect_right time used:  2.959082841873169
start = time()
for _ in range(10**6):
    index = bisect_right(lyst, 100)
print('built-in bisect_right time used: ', time()-start)
#built-in bisect_right time used:  0.5924952030181885


可见,内置的二分函数经过C语言重写,确实在效率上要高于自定义的纯python函数,大概是5倍的差距。所以在使用二分法时可优先考虑内置函数。


05 总结

  • python内置bisect模块提供了常用的二分操作,而且用C语言重写,相比自定义的二分函数有一定性能提升
  • 模块提供了有限的函数接口,但可轻松实现定制化的改造重写
  • 从编程实现上,内置函数写法简洁高效、足够pythonic,当疲于处理二分边界条件和返回值时,不妨参考一下官方写法
  • 内置的二分虽好用,但真正理解并随时会写好二分更重要



目录
相关文章
|
8月前
|
测试技术
解决Bug应有的心态和解决方法的一些思路、方法和心得
永远要相信程序是不会骗你的,是自己在处理理逻辑中出问题,而在特定的环境中才会出现或者是自己压根就想不到情况下出现。 前几天在处理一个接口任务时,在测试环境跑是一点都没有,但在正式环境却没有将数据拉下来。没有报任何错误,一度怀疑、抱怨! 还好最后找到问题解决了!
40 0
|
10月前
不是工作不好找,是你真的不行
不是工作不好找,是你真的不行
|
数据采集 算法
拒绝想当然,不看文档导致GNE 的隐秘 bug
拒绝想当然,不看文档导致GNE 的隐秘 bug
78 0
|
存储 Web App开发 缓存
一个简单的弱网差点搞死了组内前端
最近上线了一个 React Native 外访项目,用户为公司外访员,外访员根据公司业务去实地考察,收集记录一些资料,考察记录资料的过程全部用公司配的专用手机,里面安装了当前外访项目APP。目前项目试运行阶段,还没有正式交付。APP项目上线后,在用户真实使用中遇到一些各种各样的问题,有些问题处理时也比较棘手(如弱网情况),这次主要复盘APP在实际场景中的弱网(或网络不稳定)相关的问题。
778 0
一个简单的弱网差点搞死了组内前端
|
前端开发 PHP 容器
快速定位无用路由 妈妈再也不用担心人工排雷了
快速定位无用路由 妈妈再也不用担心人工排雷了
87 0
快速定位无用路由 妈妈再也不用担心人工排雷了
|
Java 中间件 程序员
最网最全bug定位套路,遇见bug再也不慌了
最网最全bug定位套路,遇见bug再也不慌了
245 0
|
芯片
程序人生 - 手上总有静电该怎么处理?
程序人生 - 手上总有静电该怎么处理?
114 0
程序人生 - 手上总有静电该怎么处理?
|
编译器 C++
萌新学习C++容易漏掉的知识点看看你中招了没有(二)
萌新学习C++容易漏掉的知识点看看你中招了没有(二)
萌新学习C++容易漏掉的知识点看看你中招了没有(二)
萌新学习C++容易漏掉的知识点,看看你中招了没有(一)
萌新学习C++容易漏掉的知识点,看看你中招了没有(一)
萌新学习C++容易漏掉的知识点,看看你中招了没有(一)
|
CDN
为啥我的网站服务器访问速度慢?网站都没人看了
载入速度对于网站、游戏、APP、视频等等来说,可以说是生死存亡的关键,如果长期访问速度不稳定,会造成客户流失、效益受损,甚至导致整体形象的损坏。而访问速度的快慢是与多方面的原因相关,我们今天可以大致了解下:
589 0

热门文章

最新文章