我们可以总结出二分查找的通用做法和常见变种。二分查找是一种在有序数组中高效查找元素的算法,时间复杂度为 O (log n)。
二分查找的通用模板
二分查找的核心思想是将搜索范围不断缩小一半,直到找到目标元素或确定其不存在。以下是通用模板:
cpp
int binarySearch(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) {return mid; // 找到目标} else if (nums[mid] < target) {left = mid + 1; // 目标在右半部分} else {right = mid - 1; // 目标在左半部分}}return -1; // 未找到目标
}
常见变种及处理技巧
1. 查找插入位置(第一个大于等于目标的位置)
- 特点:返回第一个大于等于
target
的索引,若所有元素都小于target
,则返回数组长度。 - 关键:循环结束时,
left
指向第一个大于等于target
的位置。
cpp
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 - 1;}}return left; // 最终left即为插入位置
}
2. 二维矩阵查找
- 特点:将二维矩阵展开为一维有序数组。
- 关键:计算一维索引对应的二维坐标。
cpp
bool searchMatrix(vector<vector<int>>& matrix, int target) {int m = matrix.size(), n = matrix[0].size();int left = 0, right = m * n - 1;while (left <= right) {int mid = left + (right - left) / 2;int row = mid / n, col = mid % n; // 转换为二维坐标if (matrix[row][col] == target) {return true;} else if (matrix[row][col] < target) {left = mid + 1;} else {right = mid - 1;}}return false;
}
3. 查找元素的第一个和最后一个位置
- 特点:需要两次二分查找,分别找到左边界和右边界。
- 关键:
- 左边界:第一个大于等于
target
的位置。 - 右边界:第一个大于
target
的位置减 1。
- 左边界:第一个大于等于
cpp
vector<int> searchRange(vector<int>& nums, int target) {auto lower = [&](int t) {int left = 0, right = nums.size() - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] < t) {left = mid + 1;} else {right = mid - 1;}}return left;};int first = lower(target);if (first >= nums.size() || nums[first] != target) {return {-1, -1};}int last = lower(target + 1) - 1; // 右边界return {first, last};
}
4. 旋转排序数组中的查找
- 特点:数组被旋转后,左右半部分仍有序,但整体无序。
- 关键:通过比较
nums[mid]
与nums[0]
的关系,确定哪半部分有序,再根据有序部分缩小范围。
cpp
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) {return mid;}if (nums[0] <= nums[mid]) { // 左半部分有序if (target >= nums[0] && target < nums[mid]) {right = mid - 1; // 目标在左半部分} else {left = mid + 1; // 目标在右半部分}} else { // 右半部分有序if (target > nums[mid] && target <= nums.back()) {left = mid + 1; // 目标在右半部分} else {right = mid - 1; // 目标在左半部分}}}return -1;
}
5. 寻找旋转排序数组中的最小值
- 特点:最小值是旋转点,左侧元素都大于右侧元素。
- 关键:比较
nums[mid]
与最右侧元素的大小,确定旋转点位置。
cpp
int findMin(vector<int>& nums) {int left = 0, right = nums.size() - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] <= nums[right]) { // 最小值在左侧right = mid - 1;} else { // 最小值在右侧left = mid + 1;}}return nums[left]; // 最终left指向最小值
}
二分查找的关键点总结
- 循环条件:通常为
left <= right
,确保不漏掉最后一个元素。 - 中间索引计算:使用
mid = left + (right - left) / 2
防止整数溢出。 - 边界调整:
- 若目标在右侧,
left = mid + 1
。 - 若目标在左侧,
right = mid - 1
。
- 若目标在右侧,
- 变种处理:
- 查找左边界:找到第一个大于等于
target
的位置。 - 查找右边界:找到第一个大于
target
的位置减 1。 - 旋转数组:通过比较
nums[mid]
与边界元素,确定有序部分。
- 查找左边界:找到第一个大于等于
掌握这些技巧后,可以灵活应对各种二分查找的变种问题。