普通的二分查找Ⅰ
[left,right]
int binarySearch1(vector<int> nums,int target){
int left = 0;
int right = nums.size() - 1;//这个right是可以取到的
while(left <= right){//当left==right,区间[left, right]依然有效,所以用 <=
int mid = left + (right - left) / 2;
if(target < nums[mid]){
right = mid - 1;//下次寻找区间[left, mid - 1]
}else if(target > nums[mid]){
left = mid + 1;//下次寻找区间[mid - 1 , right]
}else if(target == nums[mid]){
return mid;
}
}
return -1;
}
普通的二分查找Ⅱ
[left,right)
int binarySearch2(vector<int> nums,int target){
int left = 0;
int right = nums.size();//这个right是取不到的
while(left < right){ //right 取不到
int mid = left + (right - left)/2;
if(target < nums[mid]){
right = mid; //下次取不到 mid ,即下次的right 取不到
}else if(target > nums[mid]){
left = mid + 1;
}else if(target == nums[mid]){
return mid;
}
}
return -1;
}
1、其实模板1和模板2本质上是根据代码来区分的,而不是应用场景。如果写完之后发现是l = mid,那么在计算mid时需要加上1,否则如果写完之后发现是r = mid,那么在计算mid时不能加1
2、下取整
左加右减,右边界加一
查找符合条件的左边界
本质上查找符合if条件的第一个数(从左向右边数)
闭区间
当将区间 [l, r] 划分成 [l, mid] 和 [mid + 1, r] 时,其更新操作是 r = mid 或 l = mid + 1,计算 mid 时不需要加1,即 mid = (l + r)/2。
C++代码模板:
#include <iostream>
#include <vector>
using namespace std;
/* 二分查找模板1:在数组nums中查找target,找到返回下标值,否则返回-1;
注意,若nums中有多个一样的target值,这个模板返回的是第一次找到的target的下标位置;
最后l一定是等于r的
*/
int binSearch1(vector<int> nums, int target) {
int n = nums.size();
// 定义双指针
int l = 0, r = n - 1;
while (l < r)
{
// 二分查找,最后循环终止一定是l == r
int mid = l + r >> 1;
if (nums[mid] >= target) //target在[l,mid]
{
r = mid;
}
else {
l = mid + 1;//target在[mid +1.r]
}
}
//上面的查找满足nums[mid] >= target的第一个数,加上下面这个判断就是查找第一个target
if (nums[l] != target) {
// 没找到则返回-1
//return -1;
}
return l; // 找到返回下标
}
int main() {
vector<int> nums = { 1, 3,5,5, 7 };
int ans = binSearch1(nums, 5);
cout << ans; // 5
return 0;
}
查找符合条件的右边界
本质上查找符合if条件的第一个数(从右向左边数)
闭区间
模板2
当将区间 [l, r] 划分成 [l, mid - 1] 和 [mid, r] 时,其更新操作是 r = mid - 1 或者 l = mid,此时为了防止死循环,计算 mid 时需要加1,即 mid = ( l + r + 1 ) /2
#include <iostream>
#include <vector>
using namespace std;
/* 二分查找模板2:在数组nums中查找target,找到返回下标值,否则返回-1;
注意,若nums中有多个一样的target值,这个模板返回的是最后一次找到的target的下标位置;
*/
int binSearch2 (vector<int> nums, int target) {
int n = nums.size();
// 定义双指针
int l = 0, r = n - 1;
while (l < r) {
// 二分查找,最后循环终止一定是l == r
int mid = l + r + 1 >> 1; // 此处为什么+1呢?l会卡住,防止陷入死循环,自己举个两个元素的例子试试就懂了,例如nums = {1, 3}, target = 3;
if (nums[mid] <= target) {
l = mid;
} else {
r = mid - 1;
}
}
if (nums[l] != target) {
// 没找到则返回-1
return -1;
}
return l; // 找到返回下标
}
int main() {
vector<int> nums = {1, 3, 6, 8, 11, 13, 13, 13, 86, 100};
int ans = binSearch2(nums, 13);
cout << ans; // 7
return 0;
}
举例各种题型
一、查找一个数
1、x的平方根
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
class Solution {
public:
int mySqrt(int x) {
if(x==0) return x ;
int left = 1;
int right = x ;
while(left < right){
int mid = left + (right - left) / 2;
long mid_2 = (long) mid *mid;
if(mid_2 >= x) //x的算术平方根掉落在[l,mid]
right = mid;
else
left = mid + 1;
}
if((long)left* left != x)//在我们找不到整数的情况下,我们这个会取右边第一个比它大的整数
return left -1;
return left;
}
};
猜数字游戏的规则如下:
每轮游戏,我都会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。
如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。
你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-1,1 或 0):
-1:我选出的数字比你猜的数字小 pick < num
1:我选出的数字比你猜的数字大 pick > num
0:我选出的数字和你猜的数字一样。恭喜!你猜对了!pick == num
返回我选出的数字。
//升序
class Solution {
public:
int guessNumber(int n) {
int l = 1;
int r = n;
while(l<r){
int mid = l + (r -l) /2 ;
if (guess(mid) <= 0) //满足条件就的话,值就在[l,mid]
r = mid;
else
l = mid + 1;//不然区间就在[mid +!,r]
}
return l;
}
};
3、搜索旋转排序数组
整数数组 nums 按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
//在一个二断的区间找一个数
class Solution {
public:
int search(vector<int>& nums, int target)
{
//先判断target在那一段区间
int l = 0;
int r = nums.size()-1;
while(l < r){
int mid = l + (r -l)/2;
if(nums[mid] <= nums[r])//右半段有序
{
if(nums[mid] < target && nums[r] >= target) //要找的在[l,mid]
l = mid +1 ;
else
r= mid;
}
else//target左半段有序
{
if(nums[mid] >= target && nums[l] <= target)//要找的target在[mid+1,r]
r = mid;
else
l = mid +1;
}
}
return nums[l] == target ? l :-1;//找到了返回1,不然返回-1
}
};
二、查找左侧边界
1、第一个错误的版本
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
class Solution {
public:
int firstBadVersion(int n) {
int left = 1, right = n;
while (left < right) { // 循环直至区间左右端点相同
int mid = left + (right - left) / 2; // 防止计算时溢出
if (isBadVersion(mid)) {
right = mid; // 答案在区间 [left, mid] 中
} else {
left = mid + 1; // 答案在区间 [mid+1, right] 中
}
}
// 此时有 left == right,区间缩为一个点,即为答案
return left;
}
};
2、寻找峰值
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞ 。
你必须实现时间复杂度为 O(log n) 的算法来解决此问题。
//上坡必有顶
class Solution {
public:
int findPeakElement(vector<int>& nums)
{
int l = 0;
int r = nums.size()-1;
while(l < r){
int mid = l + (r - l) / 2;
if(nums[mid] > nums[mid+1]) //顶点在[l,mid]
r = mid;
else
l = mid +1;
}
return l;
}
};