基础知识
二分查找核心思想:不断地缩小搜索区域,降低查找目标元素的难度。
题目
1.第一个错误的版本( LeetCode 278 )
难度: 简单
题目表述:
由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。
代码(C++):
class Solution {
public:
int firstBadVersion(int n) {
int l = 1, r = n;
while (l < r) {
int mid = l + (r - l ) / 2;
if (isBadVersion(mid)) r = mid;
else l = mid + 1;
}
return l;
}
};
题解:
往左找答案,可用小结中的第一个模板
注意为了防止计算时溢出,用:l + (r - l) / 2 取代 (l + r) / 2
2.搜索插入位置( LeetCode 35 )
难度: 简单
题目表述:
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。请必须使用时间复杂度为 O(log n) 的算法。
代码(C++):
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int l = 0, r = nums.size();
while(l < r){
int mid = (r - l )/2 + l;
if(nums[mid] >= target){
r = mid;
} else l = mid + 1;
}
return l;
}
};
题解:
本质是:找到左边大于等于target的第一个数
3.搜索二维矩阵( LeetCode 74 )
难度: 中等
题目表述:
编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:
每行中的整数从左到右按升序排列。
每行的第一个整数大于前一行的最后一个整数。
代码(C++):
class Solution {
public:
// l < r 循环体内两个分支
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size(), n = matrix[0].size();
int l = 0, r = m * n - 1;
while (l < r) {
int mid = (l + r) / 2;
if (matrix[mid / n][mid % n] >= target) {
r = mid;
} else {
l = mid + 1;
}
}
// 如果答案不在[left,right],需要再做一次判断
return matrix[l / n][l % n] == target;
}
// l <= r 循环体内三个分支
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size(), n = matrix[0].size();
int l = 0, r = m * n - 1;
while (l <= r) {
int mid = (l + r) / 2;
if (matrix[mid / n][mid % n] == target) {
return true;
} if (matrix[mid / n][mid % n] > target) {
r = mid - 1;
} else {
l = mid + 1;
}
}
return false;
}
};
题解:
l < r 循环体内两个分支
l <= r 循环体内三个分支
⭐4.寻找两个正序数组的中位数( LeetCode 4 )
难度: 困难
题目表述:
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
代码(C++):
class Solution {
public:
int getKthElement(vector<int>& nums1, vector<int>& nums2, int k) {
int i = 0, j = 0, m = nums1.size(), n = nums2.size();
while (true) {
if (i == m) {
return nums2[j + k - 1];
}
if (j == n) {
return nums1[i + k - 1];
}
if (k == 1) {
return min(nums1[i], nums2[j]);
}
int new_i = min(i + k / 2 - 1, m - 1);
int new_j = min(j + k / 2 - 1, n - 1);
if (nums1[new_i] <= nums2[new_j]) {
k -= new_i - i + 1;
i = new_i + 1;
} else {
k -= new_j - j + 1;
j = new_j + 1;
}
}
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size();
int n = nums2.size();
if ((m + n) % 2 == 0) {
return double(getKthElement(nums1, nums2, (m + n) / 2) + getKthElement(nums1, nums2, (m + n) / 2 + 1)) / 2;
} else {
return double(getKthElement(nums1, nums2, (m + n) / 2 + 1));
}
}
};
题解:
本质是:查找第k小的元素,通过比较两个数组的第k / 2个元素大小,来排除掉偏小的数组中的前k / 2个元素,k再减去排除掉的实际个数来不断缩小搜索范围。还需要注意两个数组和k的越界情况。
小结
使用二分查找的前提:
- 数组具有随机访问特性,根据下标可以O(1)时间复杂度访问到元素的值,如链表不可使用二分查找
- 有序,但不是有序的也可以使用,属于特例
重要事项:
- 循环终止条件有以下两种情况:
- 可在循环中找到答案的用,
while(l <= r) ,循环体内是三个分支
; - 等到退出循环以后得到答案的用,
while(l < r) ,循环体内是两个分支
。
- 可在循环中找到答案的用,
- 剖析题目本质是找到 >= target(r= mid)还是 <= target(l = mid) 的第一个值,
=跟谁,谁就=mid
; - while(l < r) ,退出循环的时候有 l == r ,不用判断应该返回 l 还是 r;区间 [l…r] 划分只有以下两种情况:
- mid >= target:分成 [l…mid] 和 [mid + 1…r],
r = mid 和 l = mid + 1,mid = (l + r) / 2
; - mid <= target:分成 [left…mid - 1] 和 [mid…right],
r = mid - 1 和 l = mid,mid = (l + r+ 1) / 2
,否则会出现死循环。
- mid >= target:分成 [l…mid] 和 [mid + 1…r],
- 退出循环 l == r,如果可以确定区间 [l…r] 一定有解,直接返回 l 就可以,否则还需要对 l 这个位置单独做一次判断;
- 为了防止计算时整型溢出用,
l + (r - l) / 2 取代 (l + r) / 2
。
推荐模板: 第一个模板是尽量往左找目标,第二个模板是尽量往右找目标。
只要是往左找答案,就用第一个模板,mid不用加一,r=mid,l加一;
只要是往右找答案,就用第二个模板,mid要加一,l=mid,r要减一;
如果题目明确说了 要求最小值(最前面的值)还是求最大值(最后面的值),就能判断是用模板1(求最小),还是用模板2(求最大)。
之后再根据模板1,或模板2,写出对应的判断条件;
// 第一个模板
while (l < r)
{
int mid = l + r >> 1; //(l+r)/2 防止溢出用:l + (r - l) / 2
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
//如果答案不在[left,right],需要再做一次判断
// 第二个模板
while (l < r)
{
int mid = l + r + 1 >> 1; //(l+r+1)/2 防止溢出用:l + (r - l + 1) / 2
if (check(mid)) l = mid;
else r = mid - 1;
}
//如果答案不在[left,right],需要再做一次判断
参考链接
玩转 LeetCode 高频 100 题
https://blog.csdn.net/Mr_dimple/article/details/114656142/