文章目录
-
- 一、什么是二分搜索
- 二、二分搜索的适用条件
- 三、二分搜索的工作流程
- 四、二分搜索常见错误
- 五、二分搜索的代码模板(升序)
- [六、查找第一个 >= target](#六、查找第一个 >= target)
- [七、查找最后一个 <= target](#七、查找最后一个 <= target)
一、什么是二分搜索
二分搜索(Binary Search)是一种在有序数组/区间中查找目标值的位置的算法。
核心思想:每次把搜索范围缩小一半,从而快速定位数据。范围从 n 缩小到 n/2 → n/4 → n/8...
时间复杂度就是:
cpp
O(log n)
二、二分搜索的适用条件
必须满足两个条件:
- 数据有序(升序或降序)
- 可以通过下标随机访问(数组、vector 等)
不适用于链表(随机访问 O(n))
三、二分搜索的工作流程
示例:在数组中查找 target = 9
数组(升序):
cpp
索引: 0 1 2 3 4 5 6
数据: 1 3 5 7 9 11 15
初始区间:
cpp
l = 0
r = 6
第 1 次查找
cpp
区间: [0........................................6]
数据: 1 3 5 7 9 11 15
^ mid = 3 (值 = 7)
判断:
cpp
nums[mid] = 7 < 9
→ 目标在右侧
→ l = mid + 1 = 4
第 2 次查找
cpp
区间: [4....................6]
数据: 1 3 5 7 9 11 15
^ mid = 5 (值 = 11)
判断:
cpp
nums[mid] = 11 > 9
→ 目标在左侧
→ r = mid - 1 = 4
第 3 次查找
cpp
区间: [4]
数据: 1 3 5 7 9 11 15
^ mid = 4 (值 = 9)
判断:
cpp
nums[mid] == target → 找到目标
整个过程的如下展示:
cpp
Step 1:
[1 3 5 7 9 11 15]
l m r
0 3 6
Step 2:
[1 3 5 7 9 11 15]
l m r
4 5 6
Step 3:
[1 3 5 7 9 11 15]
l/m/r
4
更形象的"半区排除图"
cpp
开始:
[ 1 3 5 7 | 9 11 15 ]
↑7<9 → 左半区丢弃
继续:
[ 9 | 11 15 ]
↑11>9 → 右侧丢弃
最终:
[ 9 ]
↑ 找到!
四、二分搜索常见错误
中间点写成 (l + r) / 2 可能溢出
建议:
cpp
mid = l + (r - l) / 2;
while 条件写成 < 或 <= 搞混
闭区间写法必须:
cpp
while (l <= r)
开区间写法:
cpp
while (l < r)
不要死循环
如:
cpp
mid = (l + r) / 2;
// 然后没有改变 l 或 r
一定要注意每次都要缩小范围。
五、二分搜索的代码模板(升序)
cpp
#include <iostream>
#include <vector>
using namespace std;
// 标准二分:查找 target
int binarySearch(const vector<int>& nums, int target) {
int l = 0, r = nums.size() - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
l = mid + 1;
else
r = mid - 1;
}
return -1;
}
int main() {
vector<int> nums = {1, 3, 5, 7, 9, 11};
cout << binarySearch(nums, 7) << endl; // 输出 3
return 0;
}
为什么不能用 mid = (l+r)/2?
因为会发生「整数溢出」(overflow)
假设:
cpp
int l = 1_000_000_000;
int r = 2_000_000_000;
两者都在 int 可表示范围内,但你求 mid 时:
cpp
int mid = (l + r) / 2; // 可能溢出
l + r = 3_000_000_000,已经超过 int 最大值(2_147_483_647)
产生溢出 → mid 变成负数或奇怪值,程序会直接二分错位甚至死循环。
所以才有了 官方推荐写法:
cpp
int mid = l + (r - l) / 2;
这样 r - l 永远不会超出范围(最大约 2e9),避免溢出。
图示解释(直观)
假设区间 [l, r] = [10, 20]
错误写法:
cpp
mid = (10 + 20) / 2 = 15 // ✔ 正常
但当边界巨大:
cpp
l → 非常大
r → 非常大
l + r → 超过 int 上限 → ❌ 溢出
正确写法:
cpp
mid = l + (r - l) / 2
因为 (r - l) 很小,所以不会超出范围
结论:不能使用 (l + r) / 2 的主要原因是:当 l 和 r 足够大时,l + r 可能产生整数溢出,导致 mid 计算错误。而 l + (r - l) / 2 不会溢出,因此是安全写法。
六、查找第一个 >= target
在有序数组中查找:
cpp
第一个 >= target 的位置
假设:
cpp
数组: [1, 3, 5, 7, 9, 11, 15]
target = 8
正确答案应该是元素 9(index = 4),因为它是第一个 ≥ 8 的数字。
使用的二分区间写法(典型的 lower_bound 写法)
cpp
l = 0
r = n // r 是开区间,不可取
while (l < r)
mid = (l + r) / 2
如果 nums[mid] >= target → 可能是答案 → 缩 r
否则 → mid 太小 → 缩 l
图形演示过程:
cpp
Step 1:
[1 3 5 7 9 11 15]
l m r
0 3 7
(7 < 8 → 左半废)
Step 2:
[1 3 5 7 9 11 15]
l m r
4 5 7
(11 >= 8 → 右半废)
Step 3:
[1 3 5 7 9 11 15]
l/m r
4 5
(9 >= 8 → 右半废)
Step 4:
[1 3 5 7 9 11 15]
l=r=4
C++ 版本(对应这个图示)
cpp
int lowerBound(const vector<int>& nums, int target) {
int l = 0, r = nums.size(); // 开区间
while (l < r) {
int mid = l + (r - l) / 2;
if (nums[mid] >= target)
r = mid; // mid 可能是答案
else
l = mid + 1; // mid 太小
}
return l; // 第一个 >= target 的位置
}
七、查找最后一个 <= target
使用的二分模板(闭区间版本)
要找 "最后一个 ≤ target",一般用 闭区间 [l, r] 写法:
cpp
while (l <= r):
mid = ...
if nums[mid] <= target:
ans = mid // 可能是答案
l = mid + 1 // 尝试往右找更大的
else:
r = mid - 1
图形演示过程:
cpp
Step 1:
[1 3 5 7 9 11 15]
l m r
0 3 6
(7 <= 8 → 候选,往右找)
Step 2:
[1 3 5 7 9 11 15]
l m r
4 5 6
(11 > 8 → 往左)
Step 3:
[1 3 5 7 9 11 15]
l/m r
4 4
(9 > 8 → 往左)
Step 4:
[1 3 5 7 9 11 15]
r l
3 4
结束 → r=3
完整 C++ 代码(对应图示)
cpp
int lastLessEqual(const vector<int>& nums, int target) {
int l = 0, r = nums.size() - 1;
int ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] <= target) {
ans = mid; // 记录候选
l = mid + 1; // 去右边找更大的
} else {
r = mid - 1;
}
}
return ans; // 返回最后一次记录的位置
}