二分查找法
什么是二分查找
要解决的问题
在海量的数据中,如何以最快的速度定位到一个特定的目标? 如果数据是无序的,我们只能像 “大海捞针” 一样逐个检查(线性查找);但如果数据是有序的,我们就可以利用位置关系,成倍地缩小搜索范围。
核心思路
二分查找的核心是 “折半”。核心分成以下三个步骤:
- 取中点:查看当前搜索区间的中间元素。
- 做比较:
- 如果目标等于中点,查找成功。
- 如果目标小于中点,说明目标在左半部分,抛弃右半部分。
- 如果目标大于中点,说明目标在右半部分,抛弃左半部分。
- 循环:在新的区间内重复上述步骤,直到找到目标或区间为空。
优势和劣势
二分超找的优势:
效率极高:时间复杂度为 $O(\log n)$。即使在 40 亿个数据中查找,最多也只需要 32 次比较。
性能稳定:不像某些算法受数据分布影响波动较大。
二分查找的劣势:
- 数据必须有序:这是硬性前提。如果数据频繁变动导致无法维持有序,维护成本会很高。
- 物理存储要求:通常要求数据存储在支持随机访问(如数组)的结构中。
二分查找代码
二分查找交互演示
目标值:
RANGE [L, H]
[0, 11]
LOG
就绪
基本二分查找:这是最经典的形态,适用于数组中无重复元素,或只需找到任意一个目标的情况。
1 | /** |
查询第一个和最后一个:当数组中存在重复元素时,通过暂存结果并继续挤压区间的方法,可以精准定位边界。
1 | /** |
插值查找法
对于大致均匀分布的数据 ,我们可以在二分查找的基础上做一个小优化。在二分查找中,我们的探测点 mid 永远是:$mid = low + \frac{1}{2} \times (high - low)$。而在插值查找中,mid 变成了自适应的:
$$mid = low + \frac{target - array[low]}{array[high] - array[low]} \times (high - low)$$
1 | public static int binarySearch(int[] array, int target) { |
效率的提升:对于 100 万个均匀分布的数据,二分查找需要约 20 次,而插值查找可能只需要 3-4 次。但需要说明的是,如果数据分布极不均匀(例如 [1, 2, 4, 8, 1000, 200000]),插值查找的预测会极其离谱,效率甚至不如线性查找。
实际的应用
二分查找在工业界不仅用于查找数字,还衍生出了许多变体:
- 查找边界:在数据库索引查找中,用于定位 target 应该插入的位置,或查找第一个大于或小于某值的元素。
- 数值分析:计算一个数的平方根(在 [0, x] 范围内二分)。
- 分布式系统:一致性哈希(Consistent Hashing)中,在环状空间查找顺时针方向最近的服务节点。
- 排错:著名的 git bisect 命令,利用二分查找在成百上千次提交(commit)中快速定位引入 bug 的那一次。