1. 原理解析
二分查找,又名折半查找。 生活中的例子:假设我们在玩一个猜数字游戏,数字范围是 1 到 100。我心里想了一个数字,你每次猜一个数字,我会告诉你“大了”、“小了”还是“猜对了”。 如果你从 1 开始挨个猜(1, 2, 3...),最坏情况要猜 100 次。但如果你很聪明,你会先猜 50:
- 如果我说“大了”,你就知道答案在 1 到 49 之间。
- 下一次你猜 25,不断折半。 这就是二分查找的核心思想:每次排除一半的错误答案,极大地缩小搜索范围。
2. 使用场景
- 前提条件:数据必须是有序的(从小到大或从大到小),并且支持随机访问(比如数组)。
- 典型场景:在大量有序数据中快速定位某个特定的值,或者寻找满足某个条件的边界(如第一个大于某数的元素)。
3. 核心难点/易错点详解
二分查找看起来简单,但写对很难(被誉为“十个二分九个错”)。核心难点在于循环边界和区间更新。
关键点:明确你定义的“搜索区间”是什么。
通常我们定义搜索区间为“左闭右闭” [left, right]:
- 初始化:
left = 0,right = array.length - 1(因为索引 length-1 是可以取到的)。 - 循环条件:
while (left <= right)。为什么是<=? 因为当left == right时,区间内还有一个元素没检查,比如区间变成了[3, 3],你得把索引为 3 的元素检查一下。如果用<,就会漏掉这个元素。 - 更新区间:
- 如果
array[mid] > target,说明目标在左半边。此时新的右边界应该是right = mid - 1(因为 mid 已经检查过不是目标了,直接剔除)。 - 如果
array[mid] < target,说明目标在右半边。新的左边界left = mid + 1。
- 如果
防溢出小技巧:
计算中间位置时,不要写 mid = (left + right) / 2。如果 left 和 right 都很大,相加可能会超出整型的最大值范围导致溢出。
正确写法:mid = left + (right - left) / 2。也可以理解为“左端点加上区间长度的一半”。
4. 代码实战
4.1 左闭右闭写法 [left, right]
Python 实现:
def binary_search(nums: list[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
Java 实现:
public class BinarySearch {
public int search(int[] nums, int target) {
int left = 0;
int 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;
}
}
4.2 左闭右开写法 [left, right) (补充拓展)
Python 实现:
def binary_search_open(nums: list[int], target: int) -> int:
left, right = 0, len(nums) # right 取不到 len(nums)
# 因为 right 取不到,所以 left == right 时区间为空,循环条件不能带等号
while left < right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid # 右边界取不到,设为 mid 刚好排除 mid 本身
return -1
Java 实现:
public class BinarySearchOpen {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length;
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;
}
}
return -1;
}
}
5. 复杂度分析
- 时间复杂度:。每次循环将搜索范围缩小一半,假设总共有 个元素,最多需要找 次。这是极其高效的(比如 40 亿个数字中找一个,最多只需 32 次)。
- 空间复杂度:。我们只使用了几个整型变量(left, right, mid),没有额外申请数组空间。