核心原理与模板
二分查找不仅仅是针对有序数组,其本质是利用数据的二段性。只要存在一个分界点能将数据分为满足条件和不满足条件两部分,就可以使用二分。
关键细节
-
循环终止条件 通常使用
left <= right。当left == right时仍需比较一次,确认该位置是否为目标值。若使用left < right,则需根据具体场景调整边界更新逻辑。 -
中点计算防溢出
(left + right) / 2在大多数情况下安全,但当两数之和超过整型最大值时会溢出。推荐写法:int mid = left + (right - left) / 2;这利用了起始值加偏移量的思路,既避免了溢出又保证了结果正确。
-
奇偶数处理 当区间长度为偶数时,中间有两个数。不加 1 取左中点,加 1 取右中点。选择哪种取决于更新逻辑(
mid+1还是mid),目的是避免死循环。
经典例题实战
1. 基础查找 (LeetCode 704)
这是最朴素的二分模板。适用于严格单调递增序列。
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 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;
}
};
2. 查找元素的第一个和最后一个位置 (LeetCode 34)
此题需要分别查找左边界和右边界。由于目标值可能重复,找到相等值不能立即返回,需继续收缩区间。
查找左端点
- 当
nums[mid] < target时,左端点在右侧,left = mid + 1。 - 当
nums[mid] >= target时,左端点可能在mid处,right = mid。 - 循环条件用
left < right,避免死循环。
查找右端点
- 当
nums[mid] <= target时,右端点可能在mid处,left = mid。 - 当
nums[mid] > target时,right = mid - 1。 - 注意此时求中点需加 1:
mid = left + (right - left + 1) / 2,防止left=mid导致死循环。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if (nums.empty()) return {-1, -1};
// 查找左端点
int left = 0, right = nums.size() - 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 {-1, -1};
// 查找右端点
int l = 0, r = nums.size() - 1;
while (l < r) {
int m = l + (r - l + 1) / 2;
if (nums[m] <= target) l = m;
else r = m - 1;
}
return {left, r};
}
};
3. x 的平方根 (LeetCode 69)
虽然题目未直接给出数组,但函数 $f(x) = x^2$ 具有单调性,可视为隐式数组。
需注意乘法溢出问题,建议使用 long long 类型存储中间结果。
class Solution {
public:
int mySqrt(int x) {
if (x < 1) return 0;
int left = 1, right = x;
while (left < right) {
long long mid = left + (right - left + 1) / 2;
if (mid * mid <= x) left = mid;
else right = mid - 1;
}
return left;
}
};
4. 搜索插入位置 (LeetCode 35)
寻找第一个大于等于目标值的位置。若所有元素都小于目标值,则返回末尾索引 +1。
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) left = mid + 1;
else right = mid;
}
if (target > nums[right]) return right + 1;
return left;
}
};
5. 山脉数组的峰顶索引 (LeetCode 852)
数组先增后减,虽非全局有序,但相对于峰值具有二段性。通过比较 mid 与相邻元素判断坡度方向。
这里采用 mid 与 mid-1 比较的策略:
- 若
arr[mid] > arr[mid-1],说明处于上升段,峰值在右侧(含 mid),left = mid。 - 否则处于下降段,
right = mid - 1。
class Solution {
public:
int peakIndexInMountainArray(vector<int>& arr) {
int left = 1, right = arr.size() - 2;
while (left < right) {
int mid = left + (right - left + 1) / 2;
if (arr[mid] > arr[mid - 1]) left = mid;
else right = mid - 1;
}
return left;
}
};
6. 寻找旋转排序数组中的最小值 (LeetCode 153)
旋转数组由两段递增序列组成。以最后一个元素为基准进行划分:
- 若
nums[mid] > nums[n],说明 mid 在第一段递增区,最小值在右侧,left = mid + 1。 - 若
nums[mid] <= nums[n],说明 mid 在第二段,最小值在左侧(含 mid),right = mid。
class Solution {
public:
int findMin(vector<int>& nums) {
int n = nums.size() - 1;
int left = 0, right = n;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[n]) left = mid + 1;
else right = mid;
}
return nums[right];
}
};
7. 点名问题 (LeetCode 173)
有序数组中缺失一个数字,利用索引与值的对应关系划分区间:
- 若
records[mid] == mid,说明前半部分连续,缺失值在后半部分,left = mid + 1。 - 否则缺失值在前半部分,
right = mid。
class Solution {
public:
int takeAttendance(vector<int>& records) {
int left = 0, right = records.size() - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (records[mid] == mid) left = mid + 1;
else right = mid;
}
if (records[right] == right) return right + 1;
return right;
}
};
以上七类题型涵盖了二分查找的主要变体。掌握二段性的识别与边界条件的微调,是解决此类问题的关键。

