二分查找算法简介
二分查找的特点就是细节最多,最容易写出现死循环的算法,但是理解之后还是非常简单的。
二分算法是有模版的但是我们不建议去背模版,我们在学习二分算法中应该要理解算法的原理,我们要理解之后再记忆。
我们的模版分别有:
- 朴素的二分模版
- 查找左边界的二分模版
- 查找右边界的二分模版
2和3比较万能但是细节多
二分算法的效率很高,时间复杂度是logN
一、二分查找
题目描述:
题目分析:
题目告诉我们这个数组的元素是升序的也就是说是一个有序数组, 让我们查找target这个值的下标,如果不存在返回-1。
算法原理:
解法一:暴力解法,时间复杂度O(N)
这个题的暴力解法非常好想到,就是一个一个比较把数组遍历一遍找到了就返回下标即可,这里就不多赘述。
解法二:我们mid所在的值为中间值我们用x表示,当我们的x在左边的时候我们这段区间有没有可能大于5,就是左边紫色那一条线的区间,我们题目说了是有序数组所以我们x落在左边那段区间的时候不可能找到我们的target(5),所以我们直接让我们的left移动到mid,因为有可能我们要找的值刚刚好是mid这个下标的值我们返回即可,那么我们直接去左边寻找我们的target,但是如果我们的x在右边大于我们要找的值怎么办,那么右边是不是不可能有我们的值呀因为它大于我们的值,所以我们right = mid - 1即可。
这里我们的循环终止条件是left小于等于right,为什么是left小于等于right呢,数组里面的元素我们都是未知的当缩小成一个值的时候我们需不需要判断可能是需要判断的,我们每次移动区间的时候这段区间都是未知的,所以缩小到一个值的时候我们也是需要判断的,当我们的left大于我们的right的时候就停止循环。
我们的时间复杂度是logN
当我们的时间复杂度是O(N)我们执行4*10^9那么多数肯定超时,如果我们的时间复杂度是logN那么我们只需要执行32次几乎接近O(1),所以说这个效率非常高的。
参考代码:
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) right = mid -1;
else if(nums[mid] < target) left = mid + 1;
else return mid;
}
return -1;
}
};
总结朴素二分模版
我们本题使用二分查找算法,只要发现我们的数组有二段性都可以用我们的二分查找算法,什么是二段性呢就是当我们发现一个规律的时候根据这个规律选取某一个点并能把这个数组分成二部分,根据这个规律能舍去一部分,然而在另一端继续查找。
如下我们把if里面的条件省略其实就是我们二分查找朴素版的模版,我们的left +(right-left)/ 2和eft +(right-left + 1)/ 2的区别就是当数组是偶数个的时候+1和不+1的位置不一样,当不+1的时候我们就是中间位置的第一个位置,+1就是中间位置的第二个位置,在我们朴素模版这里两个都可以没有区分。
二、在排序数组中查找元素的第一个和最后一个位置
题目描述:
题目的意思就像如下图这样一个趋势
算法原理
解法一:暴力查找,把数组遍历一遍找到就把这个位置用变量标记一下,但是时间复杂度是O(N),题目说了要求时间复杂度是logN所以不能使用暴力解法。
解法二:这里我们用朴素二分看看
如上,假如我们使用朴素二分算法mid所在的位置直接就找到了3那么我们不能确定这个3是起始位置还是结束位置,那我们还需要往前找一下起始位置往后找结束位置,这时候我们的时间复杂度降到了O(N)。
如上图这种情况下我们的时间复杂度非常低,我们需要跑前面找开始位置去后面找结束位置。
朴素二分也可以实现但是效率不行,所以我们要换一个高效的策略。
1.查找区间的左端点
我们利用二段性,我们先查找区间的左端点也就是开始位置,如下图我们可以看到小于t的那段区间我们是不是可以舍弃不要,我们去右边查找,竟然有二段性我们就可以使用二分算法。
我们的操作和朴素二分一样的我们在朴素版里面优化一下,如下我们mid所在位置的值用x标记一下,我们x小于t说明我们所在的区间就是上面图的左边部分那么我们的left需要跳出这段区间left = mid+1,如果x大于等于t所在的位置就是右边那段区间,那么怎么更新呢,我们的right不能等于mid-1因为有可能mid就是我们要查找的区间左端点,而是right = mid
如上就是我们查找区间左端点的二分。
细节问题:
1.循环条件:
- left < right
为什么使用left < right而不是小于等于,我们分三种情况讨论
- 有结果,我们left所在的区间为不合法的区间我们的left需要跳出这段区间,而我们的right所在的区间一定是合法的因为right = mid,当left和right相遇的时候说明已经是最终结果了无需判断了。
- 全大于t,全大于t说明第一个条件一直没命中那么right往左边移,相遇之后我们只需要判断一下这个值是否等于target等于返回结果即可,不等于就按题目要求返回-1,-1
- 全小于t,和上面同理,left一直往右边移动相遇的时候只要判断一下这个值是否等于target,不等于就返回-1,-1
这里如果使用left小于等于right判断那么就会死循环,因为已经是最终结果了
2.求中点问题
所以我们求中点一定要选第一个不能选第二个,不然会死循环。
2.查找区间的右端点
大致思路和左端点差不多,但是细节处理不一样。
当我们的x小于等于target,我们需要更新left = mid因为mid有可能是最终要找的右端点
当我们的x大于target,我们需要更新right = mid - 1,因为大于target,所有那段区间是无效的我们要跳出这段区间。
求中点问题
查找右端点我们求中点要选择第二个,不然会死循环
算法原理到这里就写完了,大家可以开始编写代码了
参考代码:
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
//处理边界情况
if(nums.size() == 0) return {-1,-1};
int left = 0,right = nums.size()-1;
int begin = 0;
//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};
}
};
这里要注意边界条件下面提示了我们nums的长度可能会等于0
总结二分模版
查找左端点要用第一个求中点 ,右端点要用第二个
三、搜索插入位置
题目描述:
题目要求我们使用logN的算法,那么我们就看看能不能发现二段性,如果有二段性我们就使用二分算法。
本题使用二分算法
参考代码:
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;
}
if(nums[left] < target) return left+1;
return left;
}
};
四、x的平方根
题目描述:
解法一:暴力解法
解法二:看有没有二段性如果有就直接用二分算法
如上图本题是有二段性的那么我们使用二分算法。
如果结果小于等于x那么left = mid,如果结果大于x那么right = mid - 1
参考代码:
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;
}
};
注意边界条件,有可能x等于0
五、山脉数组的峰顶索引
题目描述:
如下图,先是递增然后递减
解法一:暴力枚举,时间复杂度O(N)
依次遍历后一个要大于前一个,当后一个小于前一个那么前一个就是顶峰元素返回下标即可。
解法二:有二段性就使用二分算法
当mid大于mid-1left = mid,如果mid小于mid-1那么right = mid - 1
参考代码:
class Solution {
public:
int peakIndexInMountainArray(vector<int>& arr) {
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 right;
}
};
六、寻找峰值
题目要求时间复杂度为logN我们可以使用二分算法
解法一:暴力解法,时间复杂度O(N)
从第一个位置开始一直向后走,分情况讨论即可
解法二:二分查找算法
通过如上我们发现了二段性,直接使用二分算法
当mid大于mid+1那么我们就去左边查找,如果小于就说明左边没有直接去mid+1那段区间找。
参考代码:
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)
解法二:题目要求时间复杂度logN,我们可以看有没有二段性如果有就是用二分算法。
如下图,AB和CD都是有递增的趋势,五角星那里也就是我们要找的最小值。
参考代码:
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0,right = nums.size()-1;
int n = nums.size();
while(left < right)
{
int mid = left + (right - left) / 2;
if(nums[mid] > nums[n-1]) left = mid + 1;
else right = mid;
}
return nums[left];
}
};
八、点名
题目描述:
本题有很多解法,在面试当中面试官会问我们这种有很多种写法的题。
本题我们有多种解法
- 哈希表
- 直接遍历找结果
- 位运算
- 数学(高斯求和公式)
- 二分查找算法
二分查找解题思路:我们可以看到值和我们的下标是对应的,那么我们判断当前的值和下标是否相等即可,只要不相等那么这个地方就是缺失值。
如下,当我们的mid落在左边区间的时候我们更新left这段区间都相等我们跳出这段区间left = right,如果不等于更新right = mid
参考代码:
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;
}
//处理边界条件
return records[left] == left ? left+1:left;
}
};
结束语
希望通过本文的介绍,您能深入理解二分查找算法的原理、实现方法及其应用场景,在实际编程中灵活运用这个高效算法,提高代码的性能和可维护性。
最后谢谢大家的支持