目录
3.153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)
5. 852. 山脉数组的峰顶索引 - 力扣(LeetCode)
🌈前言🌈
欢迎观看本期【算法杂货铺】,本期内容将讲解基础算法中的——二分算法。二分查找算法,就是利用二段性,将问题划分成两个区间,去掉一个区间,在另一个区间查找答案。
二分查找是有模板供大家使用的,即背下模板,只需要略微修改即可,但本文旨在理解这些模板。
本篇文章通过讲解例题,带大家快速了解二分算法,分为3步,讲解各个二分算法。1. 题目解析;2. 算法思路;3. 代码展示。
📁 朴素二分查找
1. 题目解析
相信有一点语言基础的人都会这道题目,我们只需要遍历一遍这个数组即可,如果找到范围下标,没有找到返回-1,但是这种暴力解法,时间复杂度是O(N)。
这里我们还有一个初始条件没有用到,即n个元素使有序的(升序)。
2. 算法原理
上图中,我们通过在一个数组中查找7,给大家展示了二分算法的基本思路。即根据某种规则,将区间划分成两端,再根据规则,舍去一端区间,在另一端区间内查找。
结束条件:
当 left > right 的时候,不存在区间,循环结束。为什么left可以等于right呢,当只有1个元素的时候,也是要查找的 , 即 (left + right )/2 = right = left 的。
为什么是正确的:
二分算法是利用二段行进行查找的,这道题的二段性体现在数组是有序的。在暴力算法中,我们是一个个比较的,在二分算法中,我们是按区间比较的,即[ left , mid-1 ] 和 [ mid+1 , right]。
时间复杂度:
我们只需要看循环了多少次即可,只要left <= right的时候,就进循环,当问题规模n就除2。
当left == right的时候,数组中只剩下一个元素,所以执行x次后,问题规模就为1。
3. 代码展示
class Solution {
public:
int search(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)
{
right = mid - 1;
}
else{
left = mid + 1;
}
}
return -1;
}
};
📂 朴素二分模板
while(left <= right)
{
//优化:防止溢出
int mid = left + (right - left)/2
if(...)
left = mid + 1;
else if(...)
right = mid - 1;
else
return mid;
}
mid = left + (right - left) /2 是进行了优化的,因为当left 和 right都很大时,很容易超出int类型的范围。mid = left + (right - left + 1) / 2在朴素二分模板中也是可以的。
+1的区别在于,偶数个数时,是向上取整,还是向下取整。
向下取整:left + ( right - left ) / 2
向下取整:left + (right - left + 1) / 2
📁 查找区间端点处
34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
1. 题目解析
依旧先从简单看起,暴力解法就是先从前往后遍历,找到第一个位置,如果找到,接着从后往前遍历,查找最后一个位置,范围begin和end。否则返回-1,-1。
暴力解法的时间复杂度是O(N)的,但题目要求我们在O(logN)的时间复杂度。在基础算法中,很少有算法能优化的logN,二分算法就是其中之一。
这道题目也是一道非常经典的二分查找算法,即查找区间的左端点,和区间的右端点。
2. 算法原理
用的还是二分算法,即二段行,根据数据的性质,在某种判断条件下将区间一分为二(不一定是有序,有二段性即可。)舍去一个区间,在另一个区间内查找。
寻找左端点思路:
左区间:[ left , result - 1 ] 都是严格小于 t 的。
右区间: [ result , right ] 都是严格大于等于 t 的。
因此,关于mid的落点,我们可以分为下面这两种情况:
1. 当 mid 落在[ left , result - 1 ] 区间的时候,即 nums[mid] < t 。说明 [left , mid ]都是可以舍去的,此时更新left到mid+1的位置,继续在 [ mid +1 , right] 上寻找左边界。
2. 当mid 落在[result , right]区间的时候,即 nums[mid] >= t。说明[mid +1 ,right]的区间可以社区(mid可能是最终结果,也可能不是),继续在[left,mid]上查找左端点。
注意的是,寻找左端点时,采用向下取整。
左指针的变化:left = mid + 1,区间逐渐减少。
右指针的变化:right = mid ,可能会原地踏步。如果采用向上取整,可能会死循环,例如:t = 2 , mid = left + (right - left + 1)/2的话,right = mid,mid = right陷入死循环。
//查找区间左端点
int left =0;
int right = nums.size()-1;
while(left < right)
{
int mid = left + (right - left)/2;
if(nums[mid] >= target)
{
right = mid;
}
else
{
left = mid + 1;
}
}
寻找右端点思路:
左区间:[ left , result ] 都是严格小于等与t的。
右区间:[ result+1 , right ] 都是严格大于t的。
1. 当mid落在左区间时,mid可能是最终结果,也可能不是,所以 left = mid,舍去区间[left,mid -1]。继续在区间[ mid , right]区间内查找。
2. 当mid落在右区间时,可以舍去[mid , right]的区间,right = mid - 1。继续在区间[ left , mid -1 ]内查找。
注意的是,寻找右端点时,采用向上取整:
右指针的变化:right = mid - 1,区间逐渐减少。
左指针的变化:left = mid,可能会原地踏步,采用向下取整,可能会陷入死循环。例如:t =1 ,mid = left + (right - 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;
}
}
细节(重要)
(1) 循环条件
left < right。我们以寻找右端点为例:
left的工作区间就是在小于等于t这个区间;right的工作区间就是在大于t这个区间。因为right = mid - 1,所以right跳出工作区间时,一定是在right == left的位置。
所以不需要判断right是否等于left,是一定等于的。
(2) 求中点操作
其实,我们只需要记住,下面有-1的时候,上面就有+1,。其他的结合题意给出二段行的判断条件。
3. 代码展示
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.size() == 0)
{
return {-1,-1};
}
//查找区间左端点
int left =0;
int right = nums.size()-1;
while(left < right)
{
int mid = left + (right - left)/2;
if(nums[mid] >= target)
{
right = mid;
}
else
{
left = mid + 1;
}
}
if(nums[left] != target)
{
return {-1,-1};
}
int begin = left;
//查找区间右端点
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;
}
}
int end = right;
return {begin,end};
}
};
📂 区间左端点处模板
while(left < right)
{
int mid = left + (right - left)/2;
if(...)
left = mid + 1;
else
right = mid;
}
📂 区间右端点处模板
while(left < right)
{
int mid = left + (right - left)/2;
if(...)
left = mid;
else
right = mid - 1;
}
以上,就将二分算法的所有知识点讲解完毕,其中朴素二分模板是容易理解的,后面两个二分模板还是需要大家自己画图不断加强理解的。
下面,我们通过几道习题加强理解。对于习题,不会再过多讲解知识点内容,而是直接给出题解思路,并配图加强理解和做题能力。
📁 习题
1. 35. 搜索插入位置 - 力扣(LeetCode)
我们通过画图,可以看出来,这是一道非常经典的查找区间左端点的题,找到大于等于t的第一个点即可。
如果t不存在在数组中,则返回left+1(left 经过循环来到right这个位置,即最后一个位置)
class Solution {
public:
int searchInsert(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)
right = mid;
else
left = mid + 1;
}
return nums[left] < target ? left + 1:left;
}
};
2. 69. x 的平方根 - 力扣(LeetCode)
其实这道题,你用二分查找左端点,或者查找右端点都可以做出来。这里我们就以查找右端点为例,当然,也会展示左端点的解法。
前面需要特殊判断x是否小于1,如果小于1,则返回0。
class Solution {
public:
int mySqrt(int x) {
if(x < 1)
{
return 0;
}
int left = 1;
int right = x;
while(left < right)
{
long long mid = left + (right - left + 1)/2;
if(mid * mid <= x)
left = mid;
else
right = mid - 1;
}
return right;
}
};
以上是查找右端点的写法,当然也可以写成查找左端点。
不过因为,查找左端点,数值是会比查找右端点大的多,所以,left 和 right都采用 long long类型。最后判断一下,right的平方是否等于x,如果不等于返回right-1。
class Solution {
public:
int mySqrt(int x) {
long long left = 0;
long long right = x;
while(left < right)
{
long long mid = left + (right - left) / 2;
if(mid * mid < x)
{
left = mid + 1;
}
else
{
right = mid;
}
}
return right*right == x?right:right-1;
}
};
这道题,可以很好的理解二分法,即一道题往往不止一种解决方法。
3.153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)
通过题意,可以了解的是,旋转就是将最后一个元素移动到最开始的位置。此外,还有一个重要条件就是 互不相同。
那我们就可以利用互不相同,得出二段行,如下图所示。
因此,我么以D点的值为索引,
当mid落在区间[A,B]时,left = mid + 1。[A,B]区间内的点是严格大于D点的值。
当mid落在区间[C,D]时,right = mid。其中C点就是我们要求的点。C点的值是要个小于D点的值。当[C,D]区间内只有一个元素时,C点的值可能等于D点的值。
class Solution {
public:
int findMin(vector<int>& nums) {
int x = nums[nums.size()-1];
int left = 0;
int right = nums.size()-1;
while(left < right)
{
int mid = left + (right - left ) / 2;
if(nums[mid] > x)
{
left = mid + 1;
}
else
{
right = mid;
}
}
return nums[right];
}
};
4. LCR 173. 点名 - 力扣(LeetCode)
数组中,下标等于元素值,如果有一位同学缺席,那么它之后所有元素的下标就不等于元素的值。
因此,我们只需要查找左端点即可。
此外,这道题还需要注意的是,最后需要处理一下边界情况,如数组中仅有一个元素1,那么缺席的同学编号就是0;如果仅有一个元素0,那么缺席的同学编号为1。
class Solution {
public:
int takeAttendance(vector<int>& records) {
int left = 0;
int right = records.size()-1;
while(left < right)
{
int mid = left + (right - left) / 2;
if(records[mid] == mid)
{
left = mid + 1;
}
else
{
right = mid;
}
}
return records[right] == right?right+1:right;
}
};
5. 852. 山脉数组的峰顶索引 - 力扣(LeetCode)
这道题目,查找左端点或查找右端点都可以,这里我们就以查找右端点为例,不在讲解如何查找左端点,感兴趣可以自己实践一下。
当mid处于上升区间时,在[mid + 1, right]区间查找。
当mid处于下降区间时,在[left , mid - 1]区间查找。
class Solution {
public:
int peakIndexInMountainArray(vector<int>& arr) {
int left =0;
int right = arr.size() -1;
while(left < right)
{
int mid = left + (right - left)/2;
if(arr[mid] < arr[mid+1])
{
left = mid +1;
}
else
{
right = mid ;
}
}
return right;
}
};
6.162. 寻找峰值 - 力扣(LeetCode)
寻找⼆段性:
1. 当arr[mid] > arr[mid+1],左侧一定存在山峰,到[left,mid]寻找山峰。
2. 当arr[mid] < arr[mid+1],右侧一定存在山峰,到[mid+1,right]寻找。
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int left =0;
int right = nums.size()-1;
while(left < right)
{
int mid = left + (right - left) / 2;
if(nums[mid] < nums[mid+1])
{
left = mid + 1;
}
else
{
right = mid;
}
}
return left;
}
};
📁 总结
以上,我们就将二分算法带大家做了全面了解。二分算法的核心就是寻找二段性,有了二段性,直接套模板即可。对于模板,建议是理解着去背诵,效果更好。
通过习题,我们也加强了对二分算法的理解,平时做题中,看到时间复杂度为logN的,我们应该敏锐的想到二分算法。
以上就是本期【算法杂货铺】的主要内容了,如果感觉对你有帮助,欢迎点赞,收藏,关注。Thanks♪(・ω・)ノ