二分
二分的思路非常直白,将每一次的取值范围都缩减为原来的一半。但是在处理边界的时候却很容易写错,很容易陷入死循环中,对此有人进行了总结,链接:二分查找为什么总是写错?_哔哩哔哩_bilibili,该视频总结的非常好,可以说是一次看懂终生难忘了。在此对该视频中讲解的内容进行总结,以便日后查阅使用(强烈建议去观看该视频!!!)。
算法模板:
int searchInsert(vector<int>& nums, int target) {
int l = -1, r = nums.size();
while(l + 1 != r){
int mid = l + (r - l) / 2;
if(condition_1){ // 根据具体情况进行条件按的选择
l = mid;
}else r = mid;
}
return l or r; // 根据具体情况进行返回
}
我们定义边界 l = -1
, r = N
,只有这样定义进行二分的时候才能包含数组中的所有下标。
证明可行性
证明是否能遍历到所有索引:
l
最小为-1
,r
的最小值为1
,因为判断条件为l + 1 != r
,所以mid
能取到的最小的索引值为(-1 + 1) / 2 = 0
;l
最大为N - 1
,r
最大为N
,那么mid
能取到最大的索引值为(N - 1 + N) / 2 = N - 1
,【注意:mid
的取值是向下取整的】。因此能够遍历到所有索引
证明是否会陷入死循环:
- 对于
r = l + 1
,根据题目条件(l + 1 != r)
,因此会退出循环。- 对于
r = l + 2
,根据判断条件,要么l = (l + r) / 2 = (l + l + 2) / 2 = l + 1
,要么r = (l + r) / 2 = (l + l + 2) / 2 = l + 1
,接着就进入了第一种情况。- 对于
r = l + 4
,根据判断条件,要么l = (l + r) / 2 = (l + l + 4) / 2 = l + 2
,要么r = (l + r) / 2 = (l + l + 3) / 2 = l + 2
,接着就进入了第二种情况,最后会进入第一种情况。- 对于其他情况,最后都会进入到第一种情况的。
因此不会陷入死循环。
如何使用判断条件?
对于数组:
1 | 2 | 3 | 4 | 5 | 5 | 5 | 6 | 7 | 8 |
---|
情况 | 判断条件1:condition_1 | 判断条件2:condition_2 |
---|---|---|
最后一个小于5的值的索引 | nums[ mid ] < 5 | l |
第一个等于5的值的索引 | nums[ mid ] < 5 | r |
最后一个等于5的值的索引 | nums[ mid ] <= 5 | l |
第一个大于5的值的索引 | nums[ mid ] <= 5 | r |
l
与 r
不会相等,循环结束l与r之间会形成一个边界,如:对于情况1(最后一个小于5的值的索引),循环结束后l 指向下标3,r
指向下标4;
举例:最后一个小于5的值的索引
int searchInsert(vector<int>& nums, int target) {
int l = -1, r = nums.size();
while(l + 1 != r){
int mid = l + (r - l) / 2;
if(nums[mid] < 5){ // 根据具体情况进行条件按的选择
l = mid;
}else r = mid;
}
return l;
}
相关题目:
整数二分例题
35. 搜索插入位置
代码
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
if (nums.empty()) return 0;
int l = -1, r = nums.size();
while(l + 1 != r){
int mid = l + (r - l) / 2;
if(nums[mid] <= target){
l = mid;
}else r = mid;
}
if(l < 0 || nums[l] < target) return r;
return l;
}
};
34. 在排序数组中查找元素的第一个和最后一个位置
代码
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> ans;
// 如果数组为空,就直接返回-1 -1
if(nums.size() == 0){
ans.push_back(-1);
ans.push_back(-1);
return ans;
}
int l = -1, r = nums.size();
// 寻找左边界
while(l + 1 != r){
int mid = (l + r) >> 1;
if(nums[mid] < target){
l = mid;
}else r = mid;
// 最后需要返回r
}
if(r >= nums.size() || nums[r] != target){ // 判断是否越界
ans.push_back(-1);
ans.push_back(-1);
return ans;
}
ans.push_back(r);
// 寻找右边界 ——> 需要向上取整
l = -1, r = nums.size();
while(l + 1 != r){
int mid = (l + r) >> 1;
if(nums[mid] <= target){
l = mid; // 最后返回l
}else{
r = mid;
}
}
ans.push_back(l);
return ans;
}
};
P2249 【深基13.例1】查找 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
代码
#include<iostream>
#include<vector>
using namespace std;
int main(){
int n, m;
cin>>n>>m;
vector<int> A(n + 1, 0);
int maxA = 0;
for(int i = 1;i < A.size(); i++){
cin>>A[i];
}
// 进行m次询问 ,第一次出现的编号
while(m--){
int target; cin>>target;
int l = 0, r = A.size();
while(l + 1 != r){
int mid = (l + r) >> 1;
if(A[mid] < target){
l = mid;
}else r = mid;
}
if(A[r] != target) r = -1;
cout<<r<<' ';
}
}
实数二分例题
69. x 的平方根
代码
class Solution {
public:
int mySqrt(int x) {
const double ACC = 1e-5;
double l = -1, r = x;
while(r - l > ACC){ // 判断精度,这里精确到小数点5位
double mid = (l + r) / 2;
if(mid * mid < x){
l = mid;
}else r = mid; // 因为是向下取整的,所以返回更大的那一个,如果返回小的由于精度的问题,会向下取整到一个错误的答案
}
return int(r);
}
};
注意这里必须使用高精度变量类型double,否则会因为精度不够而陷入死循环!!!