跳到正文
格致开物MATHWIKI

二分查找:修订间差异

AIContentBot留言 | 贡献
调整算法代码块窄屏显示:保留缩进并允许横向滚动
AIContentBot留言 | 贡献
修复 Python 代码语法高亮、行号与算法分步动画
 
第17行: 第17行:


排序条件不能省。若数列是 <math>(2,7,4)</math>,上面的左边界程序查 4 会返回下标 1,随后比较 <math>a_1=7</math> 而误判为“不存在”,尽管下标 2 存着 4。[[二分法]]则是在连续函数的异号区间内缩小求根范围;两者都对半缩小,但保持的条件不是同一件事。
排序条件不能省。若数列是 <math>(2,7,4)</math>,上面的左边界程序查 4 会返回下标 1,随后比较 <math>a_1=7</math> 而误判为“不存在”,尽管下标 2 存着 4。[[二分法]]则是在连续函数的异号区间内缩小求根范围;两者都对半缩小,但保持的条件不是同一件事。
<math-experiment type="algorithm" demo="binary-search" />


== Python 实现 ==
== Python 实现 ==
下面的两个函数不修改输入。<code>lower_bound</code> 与 <code>upper_bound</code> 分别返回左右边界;代码最后的断言就是正文数列的复算。
下面的两个函数不修改输入。<code>lower_bound</code> 与 <code>upper_bound</code> 分别返回左右边界;代码最后的断言就是正文数列的复算。


<pre class="algorithm-code" style="white-space: pre; overflow-x: auto;">
<div class="math-code-example">
<div class="math-code-language">Python 3</div>
<pre class="math-code-source" data-language="python">
def lower_bound(a, x):
def lower_bound(a, x):
     lo, hi = 0, len(a)
     lo, hi = 0, len(a)
     while lo &lt; hi:
     while lo < hi:
         mid = (lo + hi) // 2
         mid = (lo + hi) // 2
         if a[mid] &lt; x:
         if a[mid] < x:
             lo = mid + 1
             lo = mid + 1
         else:
         else:
第35行: 第39行:
def upper_bound(a, x):
def upper_bound(a, x):
     lo, hi = 0, len(a)
     lo, hi = 0, len(a)
     while lo &lt; hi:
     while lo < hi:
         mid = (lo + hi) // 2
         mid = (lo + hi) // 2
         if a[mid] &lt;= x:
         if a[mid] <= x:
             lo = mid + 1
             lo = mid + 1
         else:
         else:
第50行: 第54行:
     assert (lower_bound([], 4), upper_bound([], 4)) == (0, 0)
     assert (lower_bound([], 4), upper_bound([], 4)) == (0, 0)
</pre>
</pre>
</div>


== 代价与使用范围 ==
== 代价与使用范围 ==

2026年9月24日 (四) 02:27的最新版本

二分查找(binary search)在已经排好序的序列中寻找一个分界位置。查找“是否存在”只是它的一种用法:找到第一个不小于目标值的位置,便能同时处理重复值、插入位置和不存在的目标值。这里的输入是可按下标直接访问、按非降序排列的数列;输出是从 0 开始的下标。

一条有序数列的分界

a=(2,4,4,4,7,9,12),目标值为 4。下标 0 的值小于 4,从下标 1 起都不小于 4,所以左边界是 1;第一个大于 4 的数在下标 4,所以右边界是 4。等于 4 的元素恰占半开区间 [1,4),出现三次。若改查 5,左右边界都等于 4:5 不在数列中,但应插在下标 4。

有序数列二、四、四、四、七、九、十二的下标零到六;查找四时左边界为一,右边界为四,三个四位于半开区间一到四
左边界停在第一个 4;右边界停在 7 之前。相等元素占据两条边界之间的区间。

半开区间与查找过程

只在 [lo,hi) 内保留尚未判定的位置。左边界查找始终保持两句话:lo 左侧的元素都小于目标值,hi 及右侧的元素都不小于目标值。初始 lo=0,hi=n,两边都没有已判定元素,因此条件成立。取 mid=(lo+hi)/2

  • amid<x,从 lomid 都小于 x,把 lo 改成 mid+1
  • amidx,从 mid 到原 hi 的右侧都不小于 x,把 hi 改成 mid

在上述数列中查 4,三次比较依次发生在下标 3、1、0。区间由 [0,7) 缩到 [0,3)[0,1)[1,1)。每次都丢弃至少一个位置;当 lo=hi 时,没有未判定的位置,两侧条件共同确定了第一个不小于 x 的下标。这个结束条件也覆盖空数列,此时直接返回 0。

重复值与未命中

把比较条件改为 amidx 时,左侧留下的是“不大于 x”的元素,返回的是第一个大于 x 的位置,即右边界。两个边界之差就是出现次数;左边界小于序列长度且该处恰等于目标值时,目标值才真正存在。只在遇到相等时立刻返回一个下标,无法保证它是重复值中的第一个。

排序条件不能省。若数列是 (2,7,4),上面的左边界程序查 4 会返回下标 1,随后比较 a1=7 而误判为“不存在”,尽管下标 2 存着 4。二分法则是在连续函数的异号区间内缩小求根范围;两者都对半缩小,但保持的条件不是同一件事。

二分查找的边界移动

观察候选区间、比较位置与被排除的下标。

静态配图与完整推导见本节正文;交互演示需浏览器启用 JavaScript。

Python 实现

下面的两个函数不修改输入。lower_boundupper_bound 分别返回左右边界;代码最后的断言就是正文数列的复算。

Python 3
def lower_bound(a, x):
    lo, hi = 0, len(a)
    while lo < hi:
        mid = (lo + hi) // 2
        if a[mid] < x:
            lo = mid + 1
        else:
            hi = mid
    return lo


def upper_bound(a, x):
    lo, hi = 0, len(a)
    while lo < hi:
        mid = (lo + hi) // 2
        if a[mid] <= x:
            lo = mid + 1
        else:
            hi = mid
    return lo


if __name__ == "__main__":
    values = [2, 4, 4, 4, 7, 9, 12]
    assert (lower_bound(values, 4), upper_bound(values, 4)) == (1, 4)
    assert (lower_bound(values, 5), upper_bound(values, 5)) == (4, 4)
    assert (lower_bound([], 4), upper_bound([], 4)) == (0, 0)

代价与使用范围

每轮把候选区间至多缩为原来的一半,故比较次数为 O(logn),额外只用三个下标,为 O(1) 空间。这一时间界假定下标读取和比较都按常数成本计;链表无法像数组那样直接跳到中点。若要把元素插入 Python 列表,找到位置仍可用二分查找,但移动后续元素通常需要 O(n) 时间,不能把整个插入操作说成对数时间。

参考资料