二分查找(Binary Search)是一种在有序数组中查找特定元素的高效搜索算法。作为一名Java开发者,我几乎每周都会在工作中用到这个经典算法。它的时间复杂度仅为O(log n),比线性搜索的O(n)要高效得多,特别适合处理大规模数据集。
我第一次真正理解二分查找的威力是在处理一个用户行为日志分析项目时。当时需要在上千万条按时间排序的日志记录中快速定位特定时间段的记录,线性搜索完全无法满足性能要求,而二分查找仅需不到20次比较就能完成定位。
二分查找的核心思想是"分而治之":通过不断将搜索范围对半分割,快速缩小查找区间。这就像我们查字典时不会一页一页翻,而是根据字母顺序快速定位到大概位置。
算法需要三个关键指针:
java复制public int binarySearch(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防止整数溢出
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1; // 未找到
}
这里有几个关键点需要注意:
left + (right - left)/2而不是(left + right)/2计算mid,避免整数溢出left <= right而不是left < right,确保边界情况也被检查mid ± 1,因为mid位置已经被检查过标准的二分查找找到任意一个匹配项就返回,但实际业务中常需要找到第一个或最后一个匹配项。例如在日志系统中查找某个错误第一次出现的位置。
查找第一个匹配项的变种:
java复制public int firstOccurrence(int[] nums, int target) {
int left = 0, right = nums.length - 1;
int result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
if (nums[mid] == target) result = mid;
} else {
left = mid + 1;
}
}
return result;
}
这是面试中常见的变种问题。数组原本是有序的,但在某个点被旋转过,如[4,5,6,7,0,1,2]。查找算法需要额外判断哪一部分是有序的:
java复制public int searchInRotatedArray(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) return mid;
// 判断哪一部分是有序的
if (nums[left] <= nums[mid]) { // 左半部分有序
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else { // 右半部分有序
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
| 算法 | 最好情况 | 最坏情况 | 平均情况 | 空间复杂度 |
|---|---|---|---|---|
| 线性搜索 | O(1) | O(n) | O(n) | O(1) |
| 二分查找 | O(1) | O(log n) | O(log n) | O(1) |
对于n=1,000,000的数据集:
二分查找最容易出错的就是边界条件。以下是我总结的常见陷阱:
(left + right)/2计算mid时,当left和right都很大时会溢出while(left <= right)与while(left < right)的选择mid + 1/mid - 1而不是直接用mid当二分查找出现问题时,我通常采用以下调试方法:
java复制// 调试版二分查找
public int binarySearchDebug(int[] nums, int target) {
int left = 0, right = nums.length - 1;
System.out.println("Initial: left=" + left + ", right=" + right);
while (left <= right) {
int mid = left + (right - left) / 2;
System.out.println("Loop: left=" + left + ", mid=" + mid +
", right=" + right + ", nums[mid]=" + nums[mid]);
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
在电商平台的后台系统中,我们使用二分查找快速定位商品价格区间。商品价格预先排序后存储在内存中,当用户筛选价格区间时,使用两次二分查找:
这样可以在O(log n)时间内确定价格区间内的所有商品,即使商品数量达到千万级也能快速响应。
在一款策略游戏中,AI需要根据当前局势分数选择对应的行为策略。我们将各种局势分数预先排序,然后使用二分查找快速找到最匹配的策略:
java复制// 策略配置类
class Strategy {
int threshold;
String action;
}
// 查找最佳策略
public String findBestStrategy(Strategy[] strategies, int score) {
int left = 0, right = strategies.length - 1;
String defaultAction = "wait";
while (left <= right) {
int mid = left + (right - left) / 2;
if (strategies[mid].threshold <= score) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return right >= 0 ? strategies[right].action : defaultAction;
}
数据库的B+树索引本质上就是二分查找的扩展。理解二分查找有助于我们更好地设计数据库索引:
在大数据系统中,数据可能分布在多台机器上。我们可以使用分布式二分查找:
java复制// 伪代码:分布式二分查找
public Item distributedBinarySearch(List<Machine> machines, int key) {
// 在机器列表上执行二分查找
int left = 0, right = machines.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
Machine machine = machines.get(mid);
Range range = machine.getKeyRange();
if (range.contains(key)) {
// 在目标机器上执行本地查找
return machine.localBinarySearch(key);
} else if (range.getMax() < key) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return null;
}
在实际项目中,二分查找的应用远不止这些。掌握它的各种变体和应用场景,能够帮助我们在面对复杂问题时快速找到高效解决方案。我建议每个Java开发者都应该深入理解这个算法,并在适当场景下灵活运用。