第一次接触算法时,线性查找总是最先被介绍的基础算法。它简单直接:从数组的第一个元素开始,逐个检查每个元素,直到找到目标值或遍历完整个数组。这种暴力搜索的方式,时间复杂度是O(n),在数据量小的时候确实够用。但当我处理一个包含100万条记录的数据集时,线性查找的性能瓶颈就暴露无遗了。
记得去年优化公司的一个老系统时,遇到一个用户查询接口经常超时的问题。通过性能分析发现,核心问题就出在一个未排序数组的线性查找上。当并发请求量上来后,这个O(n)的操作直接拖垮了整个服务的响应时间。这就是为什么我们需要寻找更高效的算法——二分查找。
二分查找的精髓在于"分而治之"。它要求数据必须是有序的,然后通过不断将搜索范围对半分割来快速定位目标。想象一下在电话簿中找人的过程:你不会从第一页开始一页页翻,而是先打开中间位置,根据字母顺序决定继续向前还是向后查找。这种策略将时间复杂度从O(n)降到了O(log n)。
注意:很多人会写成mid = (low + high)/2,这在极端情况下可能导致整数溢出。使用low + (high - low)/2更安全。
让我们用Java来实现一个标准的二分查找:
java复制public int binarySearch(int[] nums, int target) {
int low = 0;
int high = nums.length - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1; // 未找到
}
这个版本虽然简单,但有几个关键点需要注意:
low <= high而不是low < high,确保能检查到边界情况mid ± 1在实际项目中,我们经常需要处理一些特殊需求。比如查找第一个或最后一个匹配的元素。这里给出查找第一个出现位置的变体:
java复制public int firstOccurrence(int[] nums, int target) {
int low = 0;
int high = nums.length - 1;
int result = -1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (nums[mid] == target) {
result = mid;
high = mid - 1; // 继续向左查找
} else if (nums[mid] < target) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return result;
}
这种变体在日志分析、时间序列数据处理等场景非常有用。
这是最直接的应用。数据库索引、缓存系统、文件系统目录等底层都大量使用二分查找。比如Redis的Sorted Set就是基于跳表实现的,而跳表的查找本质就是多层次的二分。
在科学计算中,二分法常用于求解方程的近似解。例如计算平方根:
python复制def sqrt(x, epsilon=1e-6):
low, high = 0, x
while high - low > epsilon:
mid = (low + high) / 2
if mid * mid < x:
low = mid
else:
high = mid
return (low + high) / 2
在2D/3D游戏中,经常需要对物体空间位置进行快速查询。将场景物体按坐标排序后,使用二分查找可以大幅提高碰撞检测效率。
这是一个经典面试题:假设一个排序数组在某点旋转,如[4,5,6,7,0,1,2],如何高效查找?
解决方案需要修改二分条件:
java复制public int searchInRotatedArray(int[] nums, int target) {
int low = 0, high = nums.length - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (nums[mid] == target) return mid;
if (nums[low] <= nums[mid]) { // 左半部分有序
if (nums[low] <= target && target < nums[mid]) {
high = mid - 1;
} else {
low = mid + 1;
}
} else { // 右半部分有序
if (nums[mid] < target && target <= nums[high]) {
low = mid + 1;
} else {
high = mid - 1;
}
}
}
return -1;
}
当数据是无限流(如日志流)时,传统的二分查找需要调整。可以采用指数后退策略先确定范围:
哈希表可以提供O(1)的查找复杂度,为什么还要用二分查找?
二叉搜索树也是O(log n)复杂度,但:
在分布式系统中实现二分查找时,我遇到过几个值得注意的问题:
数据分片:当数据太大无法单机存储时,需要先确定数据在哪个分片上。可以采用两级查找:先用二分确定分片,再在分片内二分查找。
一致性:如果数据在查找过程中被修改,可能导致结果不一致。在金融等敏感场景,需要考虑加锁或使用MVCC机制。
浮点数比较:处理浮点数时,直接使用==比较可能有问题。应该使用误差范围比较:
java复制if (Math.abs(nums[mid] - target) < EPSILON) {
return mid;
}