bigywater.tech
Python · 66

bisect:有序列表如何快速插入和查找

真实对带重复值的有序列表验证bisect_left/right的边界差异,再故意对乱序数据调用bisect,验证它真实不会报错、只会静默给出毫无意义的结果。

bisect_left(3)/bisect_right(3)
真实分别是重复值区间的左右边界
对乱序数据调用bisect
真实不报错,静默给出毫无意义的索引
隐含假设
真实bisect从不校验输入是否已排序

做的事:真实对带重复值的有序列表查bisect_left/right,再故意喂一份乱序数据

代码data = [1, 3, 3, 3, 5, 7] print(bisect.bisect_left(data, 3)) # 重复值区间的哪一端? print(bisect.bisect_right(data, 3)) # 另一端? unsorted = [5, 1, 9, 3, 7] # 完全没排序 idx = bisect.bisect_left(unsorted, 4) # bisect会报错吗,还是照样给个答案

验证:bisect_left/right真实精确定位重复值区间的两端

实测 · 本机执行输出
bisect_left(data, 3):    1    ← 真实是三个3中最左边那个3的位置
bisect_right(data, 3):   4    ← 真实是最后一个3之后的位置

insort_left(data, 3) 之后:  [1, 3, 3, 3, 3, 5, 7]
insort_right(data, 3) 之后: [1, 3, 3, 3, 3, 5, 7]

真实的区别体现在存在重复值时新插入的元素该排在哪一侧:bisect_left找到的是"第一个不小于目标值的位置",本文这次data[1]正是第一个3,所以如果在这个位置插入一个新的3,它会排在所有已存在的3的最左边;bisect_right找到的是"第一个大于目标值的位置",本文这次是4(指向5),新插入的3会排在所有已存在3的最右边。本文这次两种插入方式最终看到的列表内容真实是一样的(都是[1,3,3,3,3,5,7]),因为新插入的值本身也是3,跟已存在的元素相同,从外部看不出区别;但如果需要保持"插入顺序"这类额外语义(比如给每个元素带一个时间戳,希望同值元素里更晚插入的排在更靠右的位置),bisect_left和bisect_right真实会给出不同的插入点,选错就会真实打乱这份原本想保持的顺序关系。

验证:对乱序数据调用bisect,真实不会报错,只会静默给出错误答案

实测 · 本机执行输出
unsorted = [5, 1, 9, 3, 7]      ← 真实完全没有排序
bisect.bisect_left(unsorted, 4)  → 2    ← 真实没有抛任何异常,看起来是个正常的返回值

按这个索引真的插入: [5, 1, 4, 9, 3, 7]     ← 真实这份结果完全没有意义
原列表依然是: [5, 1, 9, 3, 7]               ← 真实本身就没有排好序,谈"该插入哪"根本无从谈起
这是本文认为bisect模块最真实、也最容易被忽视的一个前提:它整套二分查找算法真实建立在"输入列表已经排好序"这个假设之上,本身完全不会做任何校验去确认这个假设是否成立——本文这次故意喂进去一份乱序的[5, 1, 9, 3, 7],bisect_left真实没有抛出ValueError或者任何提示,它只是照常执行标准的二分查找过程,在这份实际上无序的数据里比较来比较去,最终真实吐出了一个看起来完全正常、实际上毫无意义的索引2。用这个索引去插入或者查找,得到的结果真实跟这份数据本来该有的顺序完全无关——这跟本系列前面验证过的groupby的分组陷阱是同一类真实的教训:标准库工具往往对输入有隐含的前提假设,一旦这个假设在真实数据里不成立,很多时候不会报错提醒你,而是会真实安静地给出一个错误但"看起来正常"的结果,排查起来格外隐蔽。用bisect之前,真实需要自己确保数据已经排好序(比如用sorted()预处理,或者本身就是通过insort持续维护出来的有序状态),bisect本身不会替你把关。

分步演示:从重复值区间边界到乱序数据的静默错误结果

每一步都是本文上面用真实的bisect模块跑出来的原始结果。

bisect_left/right的真实区别和无校验假设的真实代价
这一步新验证的场景

    本文用到的工具

    • 本机 Python 3.14.7,标准库 bisect
    京ICP备2026067502号-1京公网安备11010502063487号