本质:二分查找是用来在有序数组中快速查找目标值的最优算法
前提:只能用于有序数组/有序列表
核心思想:1.取数组的中间元素和目标值比较
2.根据大小关系,直接排除一般的元素
3.重复上述步骤,直到找到目标/确认不存在结果
cpp
如果 nums[mid] == target,找到
如果 nums[mid] < target,说明目标在右半边
如果 nums[mid] > target,说明目标在左半边
因为每次都是直接排除一半的元素->时间复杂度:
cpp
O(log n)
朴素模板:查找Target是否存在
cpp
#include <iostream>
#include <vector>
using namespace std;
int binarySearch(vector<int>& nums, int target)
{
int left = 0;
int 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.循环判断条件为 **left<=right 这是因为当left=right,**还有一个元素需要检查
2.**mid = left+(right-left)/2,**防止整型溢出
特殊模板:左端点和右端点
34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

这道题如果我们使用朴素模板的话,会发现无从下手。为了解决此类问题,我们引出了第二类二分查找模板 :第二类查找模板的差异主要体现在中点 mid 和 更新位置上
左端点:
先看示例,我们可以把数组分为两部分:小于target部分和大于等于target部分,这一部分主要用来查找符合条件的左端点即最小满足条件点!!!

如果我们的mid 落到了左侧区域(小于target 区域),我们明确知道左端点一定不会在此区域内,所以:
cpp
left = mid + 1 ;
如果我们的mid 落到了右侧区域(大于target区域), 左端点是一定出现在这个区域内的,我们在更新right 的时候就需要注意会不会跳过可能端点,所以:
cpp
right = mid;
right 直接更新到mid的位置就可以避免跳过情况的发生
接着是中点 mid
在**nums.size()**为奇数的前提下,我们不用考虑中点的位置问题,但是如果是偶数的前提下,就需要商榷了

如图所示, 如果是偶数的话,我们就需要考虑是左边的中点还是右边的中点了
在上述查找左端点的前提下,我们选择的端点就需要为 左侧的中点
因为:如果我们选择右侧的中点,那么right就会一直更新为mid这个位置,而永远不会存在left>right 这个情况的发生,导致进入死循环!!! 且 我们需要明确的一点是 当left = right 时,就是左端点,所以循环条件应该为while(left<right),如果等于直接会导致死循环!!!
右端点的判断跟左端点的判断如出一辙:

查找右端点即最大满足条件点
如果**mid<target(左侧区域),**满足条件点一定位于此区域,所以在更新left的时候需要注意:不要越过满足条件点!
cpp
left = mid;
如果**mid>target(右侧区域),**满足条件点一定不存在此区域
cpp
right = mid-1;
接着是中点问题

这两个都是偶数的中点啊,在右端点的情况下我们怎么选择呢?
当然是选择右侧的这个点了,原因跟左端点选择中点类似。如果选择左侧中点left会一直更新为mid ,而永远不会存在left = right的可能,导致陷入死循环,所以我们选择右侧中点
关于左端点还是右端点模板的代码其实还是非常简单的,唯一需要注意的就是left,right 的更新和mid的取值问题
我这里给出一个比较容易的判断方法,如果在left和right的更新中见到了 '-1'那么mid 的取值中就需要+1,即 mid = left+(right-left+1)/2;
下面给出本题的代码:
cpp
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
//
if(nums.size()==0) return {-1,-1};
//1.寻找左端点
int begin = 0;
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};
else begin = left;//标记左端点
//2.寻找右端点
//这里left直接继承就行,不用更改
right = nums.size()-1;
while(left<right)
{
int mid = left+(right-left+1)/2;
if(nums[mid]<=target) left = mid;
else right = mid-1;
}
return {begin,right};
}
};
二分查找算法题:
此类问题的关键在于:找出 二段性
LCR 072. x 的平方根 - 力扣(LeetCode)

这道题我们先来判断是朴素模板还是左端点模板或者是右端点模板
根据题意:让我们返回一个非负整数的平方根,而且类似高斯函数,返回满足条件的最大点 ,那么就很清晰了,选择右端点模板,注意mid更新为右侧中点
cpp
class Solution {
public:
int mySqrt(int x) {
if(x<1) return 0;
int left = 1,right = x;
//根据题意我们发现二段性,即左边是小于等于,右边是大于,我们需要找到左边的右端点,所以划分为
// [1,2,3][4,5,6,7] target = 10.5
while(left<right)
{
long mid = left+(right-left+1)/2;//防溢出
if(mid*mid<=x) left = mid;
else right = mid-1;//见到-1,上面+1
}
return left;
}
};

先来判断选择什么模板,可以观察到插入位置为满足条件的最大点,至于为什么我一下子就看出来了,我来简单说一下我的看法:首先明确二段性,在插入位置的左侧是小于等于target的元素,在右侧是大于target的元素,观察到这个二段性之后,我们知道注意到了插入位置是找最大点,因为数组有序,最大点即为右端点,所以选择右端点模板
cpp
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left = 0,right = nums.size()-1;
// 很明显我们发现题目具有二段性,即左边是小于target,右边是大于target,我们需要找到的是大于target中的左端点
//我们发现插入的位置右边的数字统统大于等于目标值,直接选择左端点
while(left<right)
{
int mid = left+(right-left)/2;
if(nums[mid]<target) left = mid+1;
else right = mid;
}
if(nums[nums.size()-1]<target) return nums.size();
return left;
}
};
还需要注意边界情况,其实这里可以写一个三目运算符:
cpp
return nums[nums.size()-1]<target ?left+1:left;

在普通的二分查找中,我们通过比较 arr[mid] 和 target 来决定往哪走。 而在山脉数组中,没有特定的 target,我们需要通过比较 arr[mid] 和它相邻元素(例如 arr[mid + 1])的大小关系,来判断当前处于山峰的左侧(上升段)还是右侧(下降段)。
-
情况一:
arr[mid] < arr[mid + 1]-
含义 :当前处于上升段(山峰左侧)。
-
结论 :峰顶一定在
mid的右边。因此,我们需要将左边界收缩,令left = mid + 1。
-
-
情况二:
arr[mid] > arr[mid + 1]-
含义 :当前处于下降段 (山峰右侧,或者
mid本身就是峰顶)。 -
结论 :峰顶在
mid的左边,或者就是mid本身。因此,我们需要将右边界收缩,令right = mid。
-
通过不断逼近,当 left == right 时,它们指向的位置必然就是峰顶。
cpp
class Solution {
public:
int peakIndexInMountainArray(vector<int>& arr) {
//根据分析,我们发现了这道题目的二段性,即左行的大小符合arr[mid]>arr[mid-1]
//右行的大小符合arr[mid]<arr[mid]-1,所以我们可以使用二分查找
int left = 0,right = arr.size()-1;
while(left<right)
{
int mid = left+(right-left+1)/2;
if(arr[mid]>arr[mid-1]) left = mid;
else right = mid-1;
}
return left;
}
};

这道题同理,没有Target,按趋势来作为 二段性 进行二分查找
cpp
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int left = 0,right = nums.size()-1;
while(left<right)
{
int mid = left+(right-left)/2;
//这里我们并不是很明确到底是寻找哪一个端点的话,我们可以tong过口诀 见-1 就 +1
//对于题意的思考中,我们发现在第一个 nums[mid]>nums[mid+1]中,在他的左边一定有结果,所以我们不能让right = mid-1,必须等于mid
//当 nums[mid]<=nums[mid+1],这时候右边一定右结果,所以left等于mid+1即可
if(nums[mid]>nums[mid+1]) right = mid;
else left = mid+1;
}
return left;
}
};
153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)

核心思路:与"右边界"进行博弈
在普通的二分查找中,我们通常拿 nums[mid] 和 target 比。在这道题里,由于没有 target,我们要拿 nums[mid] 和当前区间的右边界 nums[right] 进行比较,以此判断 mid 落在第一段还是第二段。
-
情况一:
nums[mid] > nums[right]-
含义 :
mid落在左边较大的那段递增区间内(例如[4, 5, 6, 7, 0, 1, 2]中的6)。 -
结论 :既然
mid比右边界还大,说明最小值一定在mid的右侧 。因此,我们要收缩左边界:left = mid + 1。
-
-
情况二:
nums[mid] < nums[right]-
含义 :
mid落在右边较小的那段递增区间内,或者数组本身就是完全升序的(没有旋转)。 -
结论 :此时
mid可能是最小值,也可能最小值的左侧还有更小的值(都在mid的左边)。因此,最小值在mid的左侧(包含mid自身) 。我们要收缩右边界:right = mid。
-