每看一次中间的数,就能排除掉一半的范围——在有序数组里,这是查找的最快方式。
想象在字典里查一个字——不会从第一页开始一页页翻,而是直接翻到中间,看看要找的字在前半部分还是后半部分,然后只在那一半里继续翻——每翻一次,能找的范围就缩小一半。二分查找就是把这个直觉写成算法:在一个已经排好序的数组里,每次检查中间的数,根据比较结果排除掉一半的范围,直到找到目标或者范围缩小到空为止。
用 left、right 维护当前还需要查找的范围。每一轮取中间下标 mid,比较 arr[mid] 和目标值:相等就直接返回;偏小说明目标在右半部分,把 left 移到 mid+1;偏大说明目标在左半部分,把 right 移到 mid-1。
| 1 | int BinarySearch(vector<int> arr, int target) |
| 2 | { |
| 3 | int left = 0, right = arr.size() - 1; |
| 4 | while (left <= right) |
| 5 | { |
| 6 | int mid = left + (right - left) / 2; |
| 7 | if (arr[mid] == target) |
| 8 | { |
| 9 | return mid; |
| 10 | } |
| 11 | else if (arr[mid] < target) |
| 12 | { |
| 13 | left = mid + 1; // 目标在右半部分 |
| 14 | } |
| 15 | else |
| 16 | { |
| 17 | right = mid - 1; // 目标在左半部分 |
| 18 | } |
| 19 | } |
| 20 | return -1; // 没找到 |
| 21 | } |
mid = left + (right - left) / 2,不直接写 (left + right) / 2?这是为了防止溢出——如果 left 和 right 都已经是很大的数(接近 int 的上限),left + right 这一步加法本身就可能先溢出,哪怕最终算出来的 mid 没什么问题。left + (right - left) / 2 先算两者的差(不会比 right 本身大),再加回 left,全程不会经过比 right 更大的中间值,是更安全的写法——和 1.5 节最小公倍数公式"先除再乘"防止溢出是同一种思路。11 个数的数组,只用了 4 次比较就找到了目标——如果用最朴素的线性查找(从头一个个比对),最坏情况要比较 11 次。每一轮被排除的区域都越来越大,这正是二分查找比双指针更快的原因:双指针每次最多排除一个元素,二分查找每次能排除大约一半的范围。7.1 节的对撞指针和这一节的二分查找,本质上都是利用数组的有序性,不断排除"不可能存在答案"的部分。区别在于排除的速度:对撞指针每一步只能确定排除一个元素(left 或 right 挪一格);二分查找每一步能直接排除大约一半的剩余范围——这也是为什么二分查找的时间复杂度是 O(log n),比双指针的 O(n) 还要快得多。
| 对撞指针 | 二分查找 | |
|---|---|---|
| 每一步排除的范围 | 一个元素 | 大约一半的剩余范围 |
| 时间复杂度 | O(n) | O(log n) |
| 典型场景 | 需要同时关注两个位置的配对关系(如两数之和) | 只需要判断"在不在、往哪边找" |
数组长度 n=1000000(一百万):
log2(1000000) ≈ 19.9,最多 20 次比较就能确定结果(或者确认不存在)——数据规模越大,二分查找的优势越是碾压性的。这也是为什么"判断能不能用二分",往往是看到"有序"两个字之后第一个该想到的优化方向。| 查找方式 | 前提条件 | 时间复杂度 |
|---|---|---|
| 线性查找 | 无(数组顺序任意) | O(n) |
| 二分查找 | 数组必须已经排好序 | O(log n) |