1. 二分查找算法基础与进阶
作为一名算法工程师,我经常需要在海量数据中快速定位目标元素。二分查找算法就像一把精准的手术刀,能在O(log n)时间复杂度内完成搜索任务。今天我想和大家分享二分查找的核心原理和实际应用中的各种变体。
1.1 基础二分查找原理
二分查找的基本思想很简单:对于一个有序数组,每次将搜索范围缩小一半。具体实现时需要注意几个关键点:
- 循环条件:通常使用left <= right,确保能处理所有元素
- 中点计算:推荐使用left + (right - left)/2避免整数溢出
- 边界更新:根据比较结果决定是更新左边界还是右边界
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) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
return mid;
}
}
return -1;
}
注意:在Java等语言中,(left + right)/2可能导致整数溢出,使用left + (right - left)/2更安全。
1.2 二分查找的变体与应用场景
二分查找的强大之处在于它的变体可以解决各种边界问题。以下是几种常见变体:
- 查找第一个等于target的元素
- 查找最后一个等于target的元素
- 查找第一个大于等于target的元素
- 查找最后一个小于等于target的元素
这些变体的核心区别在于:
- 中点计算方式(左中点还是右中点)
- 边界更新条件
- 循环终止条件
2. 查找元素的边界位置
2.1 查找第一个和最后一个位置
LeetCode第34题要求我们在有序数组中查找目标值的开始和结束位置。这个问题需要两次二分查找:
java复制public int[] searchRange(int[] nums, int target) {
int[] result = {-1, -1};
if (nums.length == 0) return result;
// 查找左边界
int left = 0, right = nums.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
if (nums[left] != target) return result;
result[0] = left;
// 查找右边界
right = nums.length - 1;
while (left < right) {
int mid = left + (right - left + 1) / 2;
if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid;
}
}
result[1] = right;
return result;
}
2.2 关键点解析
-
左边界查找:
- 使用左中点(mid = left + (right-left)/2)
- 当nums[mid] >= target时,right = mid
- 循环条件为left < right
-
右边界查找:
- 使用右中点(mid = left + (right-left+1)/2)
- 当nums[mid] <= target时,left = mid
- 循环条件同样为left < right
实际项目中,这种边界查找常用于日志时间范围查询、用户行为分析等场景。
3. 搜索插入位置与平方根计算
3.1 搜索插入位置
LeetCode第35题要求在有序数组中找到目标值应插入的位置。这个问题可以转化为查找第一个大于等于目标值的元素位置。
java复制public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
return mid;
}
}
return left;
}
3.2 计算平方根
LeetCode第69题要求计算非负整数的平方根。这个问题可以转化为查找最大的整数使得其平方小于等于目标值。
java复制public int mySqrt(int x) {
if (x < 2) return x;
long left = 1, right = x;
while (left < right) {
long mid = left + (right - left + 1) / 2;
if (mid * mid > x) {
right = mid - 1;
} else {
left = mid;
}
}
return (int)left;
}
工程实践中,这种方法也适用于其他单调函数的近似计算。
4. 山脉数组与峰值查找
4.1 山脉数组的顶峰索引
LeetCode第852题要求找出山脉数组的顶峰索引。山脉数组的特点是先递增后递减。
java复制public int peakIndexInMountainArray(int[] arr) {
int left = 0, right = arr.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] < arr[mid + 1]) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
4.2 寻找峰值元素
LeetCode第162题要求在可能包含多个峰值的数组中找到任一峰值的位置。
java复制public int findPeakElement(int[] nums) {
int left = 0, right = nums.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < nums[mid + 1]) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
这类问题在信号处理、股票分析等领域有广泛应用。
5. 旋转排序数组中的最小值
5.1 基本解法
LeetCode第153题要求在旋转排序数组中找到最小元素。旋转排序数组是指将有序数组的前面一部分元素移动到数组末尾。
java复制public int findMin(int[] nums) {
int left = 0, right = nums.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) {
left = mid + 1;
} else {
right = mid;
}
}
return nums[left];
}
5.2 处理重复元素的情况
当数组中可能存在重复元素时,算法需要稍作调整:
java复制public int findMinWithDuplicates(int[] nums) {
int left = 0, right = nums.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) {
left = mid + 1;
} else if (nums[mid] < nums[right]) {
right = mid;
} else {
right--;
}
}
return nums[left];
}
6. 缺失数字的查找
6.1 二分查找解法
LeetCode LCR 173题要求在0~n-1的递增排序数组中找出唯一缺失的数字。
java复制public int missingNumber(int[] nums) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == mid) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
6.2 其他解法比较
除了二分查找,这个问题还可以用以下方法解决:
- 异或法:时间复杂度O(n),空间复杂度O(1)
- 数学求和法:时间复杂度O(n),空间复杂度O(1)
- 哈希表法:时间复杂度O(n),空间复杂度O(n)
在实际工程中,当数据量不大时,简单的遍历可能更易维护;数据量大时,二分查找的优势就体现出来了。
7. 二分查找的工程实践技巧
7.1 调试技巧
- 打印关键变量:在循环中打印left、right、mid的值
- 边界测试:测试空数组、单元素数组、全相同元素数组等特殊情况
- 可视化辅助:画出搜索区间变化图
7.2 性能优化
- 使用位运算代替除法:mid = (left + right) >>> 1
- 避免重复计算:将频繁使用的值缓存起来
- 循环展开:在特定场景下可以减少循环次数
7.3 常见错误
- 死循环:通常由于边界更新不当导致
- 漏掉元素:循环条件设置不当
- 整数溢出:如前所述,使用安全的中间值计算方法
8. 二分查找的扩展应用
二分查找的思想可以应用于许多非传统场景:
- 在无限序列中查找元素
- 求解单调函数的最值问题
- 资源分配问题(如书籍分配、任务调度)
- 机器学习中的超参数调优
我在实际项目中曾用二分查找优化过一个分布式系统的负载均衡算法,将均衡时间从O(n)降低到O(log n),显著提升了系统性能。