🔍 二分查找算法及变种形式详解
📖 概述
二分查找(Binary Search)是计算机科学中最基础、最重要的算法之一。它通过每次将搜索范围缩小一半的方式,在有序数组中高效地查找目标元素,时间复杂度为 O(log n)。
🎯 核心思想
二分查找的核心思想是:分治。通过比较中间元素与目标值,每次都能将搜索范围缩小一半,直到找到目标值或确定不存在。
🔢 基础二分查找
标准模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class BinarySearch {
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; } }
|
基础二分查找测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Test public void testBasicBinarySearch() { BinarySearch bs = new BinarySearch(); int[] nums = {1, 3, 5, 7, 9, 11, 13, 15}; assertEquals(0, bs.binarySearch(nums, 1)); assertEquals(7, bs.binarySearch(nums, 15)); assertEquals(3, bs.binarySearch(nums, 7)); assertEquals(-1, bs.binarySearch(nums, 0)); assertEquals(-1, bs.binarySearch(nums, 16)); assertEquals(-1, bs.binarySearch(nums, 6)); }
|
🔄 二分查找的变种形式
1. 查找第一个等于目标值的元素
场景:数组中可能有重复元素,需要找到第一个等于目标值的位置
原始实现(可优化)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
public int findFirstEqual(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 { if (mid == 0 || nums[mid - 1] != target) { return mid; } else { right = mid - 1; } } } return -1; }
|
优化实现一:简化逻辑版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public int findFirstEqualOptimized(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] >= target) { right = mid - 1; } else { left = mid + 1; } } return (left < nums.length && nums[left] == target) ? left : -1; }
|
2. 查找最后一个等于目标值的元素
原始实现(可优化)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
public int findLastEqual(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 { if (mid == nums.length - 1 || nums[mid + 1] != target) { return mid; } else { left = mid + 1; } } } return -1; }
|
优化实现一:简化逻辑版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
public int findLastEqualOptimized(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 { right = mid - 1; } } return (right >= 0 && nums[right] == target) ? right : -1; }
|
3. 查找第一个大于等于目标值的元素
原始实现(可优化)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
public int findFirstGreaterOrEqual(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 (mid == 0 || nums[mid - 1] < target) { return mid; } else { right = mid - 1; } } } return -1; }
|
优化实现一:简化逻辑版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
public int findFirstGreaterOrEqualOptimized(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] >= target) { right = mid - 1; } else { left = mid + 1; } } return (left < nums.length && nums[left] >= target) ? left : -1; }
|
4. 查找最后一个小于等于目标值的元素
原始实现(可优化)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
public int findLastLessOrEqual(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] > target) { right = mid - 1; } else { if (mid == nums.length - 1 || nums[mid + 1] > target) { return mid; } else { left = mid + 1; } } } return -1; }
|
优化实现一:简化逻辑版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
public int findLastLessOrEqualOptimized(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 { right = mid - 1; } } return (right >= 0 && nums[right] <= target) ? right : -1; }
|
🎯 应用案例详解
案例1:在排序数组中查找元素的第一个和最后一个位置
LeetCode 34:给定一个按照升序排列的数组,找到目标值的开始和结束位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| public class Solution {
private int lowerBound(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] >= target) { right = mid - 1; } else { left = mid + 1; } } return left; } public int[] searchRange(int[] nums, int target) { int start = lowerBound(nums, target); if (start == nums.length || nums[start] != target) { return new int[]{-1, -1}; } int end = lowerBound(nums, target + 1) - 1; return new int[]{start, end}; } }
|
案例2:搜索插入位置
LeetCode 35:给定一个排序数组和一个目标值,如果目标值存在于数组中,返回其索引;如果不存在,返回它应该被插入的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class Solution { 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) { return mid; } else if (nums[mid] < target) { left = mid + 1; } else { right = mid - 1; } } return left; } }
|
案例3:搜索旋转排序数组
LeetCode 33:在旋转过的有序数组中查找目标值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class Solution { public int search(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; } }
|
案例4:寻找旋转排序数组中的最小值
LeetCode 153:在旋转排序数组中找到最小值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class Solution { 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:x 的平方根
LeetCode 69:实现 int sqrt(int x) 函数,返回 x 的平方根的整数部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class Solution { public int mySqrt(int x) { if (x == 0) return 0; int left = 1, right = x; while (left <= right) { int mid = left + (right - left) / 2; if (mid <= x / mid && (mid + 1) > x / (mid + 1)) { return mid; } else if (mid > x / mid) { right = mid - 1; } else { left = mid + 1; } } return right; } }
|
案例6:购物系统降级规则
实际应用场景:购物APP核心系统面临集群故障,需要设计降级规则来限制客户端调用量。
问题描述:
- 有N个客户端,每个客户端有不同的调用量R = [R₁, R₂, …, Rₙ]
- 核心系统最大调用量为cnt
- 如果总调用量sum® ≤ cnt,返回-1(正常调用)
- 如果总调用量sum® > cnt,需要设定阈值value
- 超过value的客户端限制为value,求最大可能的value
解决方案:使用二分查找寻找最大阈值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| public class ShoppingSystemDegradation {
public int findDegradationThreshold(int[] R, int cnt) { long total = 0; for (int num : R) { total += num; } if (total <= cnt) { return -1; } int left = 0, right = getMax(R); while (left <= right) { int mid = left + (right - left) / 2; long sumWithThreshold = calculateSumWithThreshold(R, mid); if (sumWithThreshold <= cnt) { left = mid + 1; } else { right = mid - 1; } } return right; }
private long calculateSumWithThreshold(int[] R, int threshold) { long sum = 0; for (int num : R) { sum += Math.min(num, threshold); } return sum; }
private int getMax(int[] R) { int max = 0; for (int num : R) { max = Math.max(max, num); } return max; } public static void main(String[] args) { ShoppingSystemDegradation solution = new ShoppingSystemDegradation(); int[] R1 = {5, 3, 8, 2, 7}; int cnt1 = 15; System.out.println("测试用例1:" + solution.findDegradationThreshold(R1, cnt1)); int[] R2 = {2, 3, 1, 4}; int cnt2 = 20; System.out.println("测试用例2:" + solution.findDegradationThreshold(R2, cnt2)); int[] R3 = {10, 10, 10}; int cnt3 = 25; System.out.println("测试用例3:" + solution.findDegradationThreshold(R3, cnt3)); } }
|
算法分析:
- 时间复杂度:O(n log m),其中n是客户端数量,m是最大调用量
- 空间复杂度:O(1)
- 核心思想:通过二分查找快速定位最大阈值,避免线性搜索
⚡ 性能分析
时间复杂度
- 基础二分查找:O(log n)
- 所有变种形式:O(log n)
空间复杂度
- 迭代实现:O(1)
- 递归实现:O(log n)(递归栈空间)
性能对比
| 算法 |
时间复杂度 |
空间复杂度 |
适用场景 |
| 线性搜索 |
O(n) |
O(1) |
无序数组 |
| 二分查找 |
O(log n) |
O(1) |
有序数组 |
| 哈希表查找 |
O(1) 平均 |
O(n) |
快速查找 |
🛠️ 实战练习
练习题1:寻找峰值
描述:峰值元素是指其值严格大于左右相邻值的元素。给定一个数组,找到峰值元素并返回其索引。
要求:时间复杂度 O(log n)
练习题2:搜索二维矩阵
描述:编写一个高效的算法来搜索 m × n 矩阵中的一个目标值。该矩阵具有以下特性:
- 每行中的整数从左到右按升序排列
- 每行的第一个整数大于前一行的最后一个整数
要求:时间复杂度 O(log(m × n))
练习题3:寻找重复数
描述:给定一个包含 n + 1 个整数的数组,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的数字。假设只有一个重复的数字,找出这个重复的数字。
要求:
- 不能修改原数组(假设数组是只读的)
- 只能使用额外的 O(1) 的空间
- 时间复杂度小于 O(n²)
📚 相关LeetCode题目
基础题目
进阶题目
高难度题目
🎯 关键要点总结
核心原则
- 有序性:二分查找的前提是数组必须有序
- 边界条件:注意处理 left、right、mid 的边界情况
- 溢出处理:使用
left + (right - left) / 2 而不是 (left + right) / 2
- 终止条件:理解
while (left <= right) 和 while (left < right) 的区别
常见陷阱
- 死循环:注意更新 left 和 right 时的边界条件
- 数组越界:检查 mid 的相邻元素是否存在
- 精度问题:处理整数溢出和浮点数比较
- 特殊情况:空数组、单元素数组等边界情况
优化技巧
- 位运算优化:使用
mid = left + ((right - left) >> 1) 提高性能
- 早停策略:在某些情况下可以提前终止搜索
- 缓存中间结果:避免重复计算
📖 参考资料
最后更新:2025-01-15