二分算法简介:
- 二分查找算法只适用于数组有序的情况?(只要数组中存在某种规律就可以用)
- 模版:
- 朴素的二分模版
- 查找左边界的二分模版
- 查找右边界的二分模版
while(left <= right)
{
int mid = left+ (right-left+1)/2 ; //防止溢出
if(...)
left = mid+1;
else if(...)
right = mid-1;
else
return ...;
}
二分查找
暴力解法:从头到尾遍历,若找到目标值返回下标,否则返回-1。时间复杂度O(n),但并没有利用数组的有序性
一个数组中,随机找一个数,去和target进行比较,做完比较之后,划分出两个区域;其中根据规律可以有选择性的舍去一个规律,继续在另一个规律中寻找。此时该题目具有二段性,我们可以用二分查找算法(这里选取二分之一是最优选择,因为他的时间复杂度以及根据概率学问题是最优的)
朴素二分查找算法:我们来探讨一下细节问题
class Solution {
public:
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) left = mid+1;
else if(nums[mid] > target) right = mid-1;
else return mid;
}
return -1;
}
};
在排序数组中查找元素的第一个和最后一个位置
寻找左边界
寻找左边界:retLeft 表⽰左边界, retRight 表⽰右边界
我们注意到以左边界划分的两个区间的特点:
因此,关于mid的落点,我们可以分为下⾯两种情况:
当我们的 mid 落在 [left, retLeft - 1] 区间的时候,也就是 arr[mid]
当mid落在 [retLeft, right] 的区间的时候,也就是 arr[mid] >= target 。说明 [mid + 1, right] (因为 mid 可能是最终结果,不能舍去)是可以舍去的,此时更新 right 到 mid 的位置,继续在 [left, mid] 上寻找左边界
细节处理:
循环条件:这里要写成left
求中点操作:若用第二个,则mid会落在right的位置上,此时如果进入第一个循环条件没问题,但是进入第二个循环条件,right位置没有动,求出的mid还是这个位置,下次进入循环条件时还是进入第二个,所以会进入死循环
注意:这⾥找中间元素需要向下取整。因为后续移动左右指针的时候:
- 左指针: left = mid + 1 ,是会向后移动的,因此区间是会缩⼩的;
- 右指针: right = mid ,可能会原地踏步(⽐如:如果向上取整的话,如果剩下 1,2 两个元素, left == 1 , right == 2 , mid == 2 。更新区间之后, left,right,mid 的值没有改变,就会陷⼊死循环)。因此⼀定要注意,当 right = mid 的时候,要向下取整。
寻找右边界
我们注意到右边界的特点:
class Solution
{
public:
vector<int> searchRange(vector<int>& nums, int target)
{
// 处理边界情况
if(nums.size() == 0) return {-1, -1};
int begin = 0;
// 1. ⼆分左端点
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 = 0, 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};
}
};
x的平方根
class Solution {
public:
int mySqrt(int x)
{
if(x < 1) return 0; // 处理边界情况
int left = 1, right = x;
while(left < right)
{
long long mid = left + (right - left + 1) / 2; // 防溢出
if(mid * mid <= x) left = mid;
else right = mid - 1;
}
return left;
}
};
搜索插入位置
二分查找:
综上所述,插入位置ret最终应该大于等于目标值,因此通过ret将数组划分为两个区间,此时发现二段性,用二分查找。所以我们只需要找到大于等于目标值区间的左端点即可。所以我们套用查找区间左端点的模板
class Solution {
public:
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;
}
//此时right和left已经相遇,所以这里返回谁都可以
if(nums[left] < target) return right + 1; //插入位置在数组最后一个位置
return right;
}
};
山脉数组的峰顶索引
当数字落在红色箭头这一列时,峰值是在这一区间的右端点,所以不能跳过,故更新left位置时,left=mid;
当数字落在蓝色箭头这一列时,峰值一定不会在该区域,所以更新right位置时,right = mid-1;
![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2023%2Fpng%2F29339358%2F1701759796953-a2869478-00eb-45f8-8a7a-6bdc4f7be763.png%23averageHue%3D%2523fefefe%26clientId%3Due138c47e-15f4-4%26from%3Dpaste%26height%3D431%26id%3Dud9b4daa7%26originHeight%3D539%26originWidth%3D716%26originalType%3Dbinary%26ratio%3D1.25%26rotation%3D0%26showTitle%3Dfalse%26size%3D124540%26status%3Ddone%26style%3Dnone%26taskId%3Du823856d0-63c3-47fb-9831-9f68416db8b%26title%3D%26width%3D572.8&pos_id=img-eMGVQ748-1701766742087)
class Solution {
public:
int peakIndexInMountainArray(vector<int>& arr) {
int left = 1,right = arr.size()-2; //这里不让left=1,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;
}
};
寻找峰值
该题严格无序数组,可能出现多个山峰的情况
故得出二段性,使用二分查找。
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int left = 0,right = nums.size()-1;
while(left < right)
{
int mid = left+(right-left)/2;
if(nums[mid] > nums[mid+1]) right = mid;
else left = mid+1;
}
return left;
}
};
寻找旋转排序数组中的最小值
解法一:暴力解法:遍历查找最小值,时间复杂度为O(n)
解法二:二分查找:可以把旋转后的数组分为两部分,两部分都是呈现递增的形式,把该问题抽象成折线图,我们会发现明显的二段性,我们以D点作为参照物,会发现折线AB上的值全部大于D点的值,折线CD上的值都小于或等于D点的值。所以我们找到C点对应的值即为最小值即可
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0,right = nums.size()-1;
int x =nums[right]; //表示最后一个位置的值
while(left < right)
{
int mid = left+(right-left)/2;
if(nums[mid] > x) left = mid+1;
else right=mid;
}
return nums[left];
}
};
0~n-1缺失的数字
⼀个⻓度为n-1的递增排序数组中的所有数字都是唯⼀的,并且每个数字都在范围0〜n-1之内。在范围0〜n-1内的n个数字中有且只有⼀个数字不在该数组中,请找出这个数字
以上解法时间复杂度都是O(n),哈希表解法中还有个O(n)的空间复杂度
解法五:二分查找:左边区间数组下标的值与正常的数组下标一一对应,右边区间数组中数字下标与正常的不是一一对应;即发现该数组的二段性。我们需要找的结果就在五角星位置,即右边区间最左边的位置
class Solution {
public:
int takeAttendance(vector<int>& records)
{
int left = 0, right = records.size() - 1;
while(left < right)
{
int mid = left + (right - left) / 2;
if(records[mid] == mid) left = mid + 1;
else right = mid;
}
//返回left和right都行
//处理细节问题
return left == records[left] ? left + 1 : left;
}
};