【算法】二分查找
正确认识二分算法
二分查找的特点
其实二分查找算法才是最恶心、细节最多、最容易写出死循环的算法,通过本篇博客让二分成为最简单的算法。
二分算法的使用场景
我们曾经进行听过二分算法的时候都是说数组必须有序,这其实是不准确的,只要是数组具有二段性,即使数组不是有序的依然可以进行使用二分查找算法。
二分查找的时间复杂度
考虑最坏的情况,使用二分查找时查找的最后一个元素才是目标元素,每次进行筛选的元素个数都是新数组的一半,也就是说2的x次方等于n,进行化简x=logN,时间复杂度是longN。
二分查找的三个模板
-
朴素的二分模板
int left=0;
int right=nums.size()-1;
int mid=-1;
while(left<=right) //细节1
{
mid=left+(right-left)/2; //细节2
if(......)
{
right=mid-1;
}
else if(......)
{
left=mid+1;
}
else
{
return ......;
}
}
两个细节
进行循环的判断条件必须是left<=right,因为当数组中其他的元素都筛选完毕后,只剩下一个元素,此时left和right都指向的都是该元素,也是需要进行判断的。
中间位置的记录要通过left+(right-left)/2进行记录,left+(right-left)/2的含义是从left开始进行移动(right-left)/2个位置,不建议通过(left+right)/2的方式进行,因为这种方式存在溢出的风险。
查找左边界的二分模板
while(left<right)
{
int mid=left+(right-left)/2;
if(......)
{
left=mid+1;
}
else
{
right=mid;
}
}
return left;
三个细节
进行更新右指针时必须进行更新到mid位置,否则容易进行错过左端点
循环的的判断条件必须是left<right,当两个指针指向同一个位置时就是结果,无需进行判断,否则会出现死循环,出现死循环的情况发生在区间中的值全部大于目标值的情况left和right指针进行重合的时候就会出现死循环
求中点的操作必须是left+(righ-left)/2,不可以是left+(righ-left+1)/2,否则在区间中有结果的情况下也是会出现死循环,当mid位置的值就是目标值时就会死循环。
查找右边界的二分模板
while(left<right)
{
int mid=left+(right-left+1)/2;
if(......)
{
left=mid;
}
else
{
right=mid-1;
}
}
return left;
分析思路和查找最边界的分析思路相同
三个细节
进行更新左指针时必须进行更新到mid位置,否则容易进行错过左端点
循环的的判断条件必须是left<right,当两个指针指向同一个位置时就是结果,无需进行判断,否则会出现死循环,出现死循环的情况发生在区间中的值全部大于目标值的情况left和right指针进行重合的时候就会出现死循环
求中点的操作必须是left+(righ-left+1)/2,不可以是left+(righ-left+1)/2,否则在区间中有结果的情况下也是会出现死循环,当mid位置的值就是目标值时就会死循环。
704、二分查找
题解
暴力
这道题比较简单,直接进行遍历查找即可,时间复杂度为O(n)
二分查找
通过暴力进行查找每次进行过滤掉的不合格的元素只有一个造成时间复杂度较高,能不能一次进行过滤掉多个不合格的元素进行提高时间复杂度呢?一次进行筛选数组的二分之一、三分之一、n分之一都是可以的,但是通过数学的期望的论证,选二分之一在大多数的情况下的效率是最高的,这就是大名鼎鼎的二分查找。
class Solution {
public:
int search(vector<int>& nums, int target)
{
int left=0;
int right=nums.size()-1;
int mid=-1;
while(left<=right)
{
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;
}
};
34、在排序的数组中查找数组的第一个元素和最后一个元素
题解
暴力
暴力非常简单,直接进行循环查找目标元素,当查找到目标元素时的这个位置就是左端点进行通过另一个for循环进行确定右端点,时间复杂度是O(n)
二分查找
题目直接告诉我们了需要进行查找数组的第一个元素和最后一个元素,这个就是上面进行分析的模板,直接进行套模板即可。这道题简单就简单在直接告诉我们这是需要进行查找符合条件的子数组的左右端点,直接就对应的就是二分查找左边界和右边界模板。
使用二分查找的前提条件就是数组必须具有二段性,以下是二段性的分析
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;
vector<int> ret;
while(left<right)
{
int mid=left+(right-left)/2;
if(nums[mid]<target)
{
left=mid+1;
}
else
{
right=mid;
}
}
if(nums[left]==target)
{
ret.push_back(left);
}
else
{
ret.push_back(-1);
}
//进行查找右端点
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;
}
}
if(nums[left]==target)
{
ret.push_back(left);
}
else
{
ret.push_back(-1);
}
return ret;
}
};
35、搜索插入的位置
题解
题目中要求要在O(log n)的时间复杂度进行解决问题,所以说直接不能够使用暴力了。
二分查找
为什么可以使用二分查找进行解决该题目?
数组是具有二段性的,二段性如下图所示
将数组通过二段性进行划分小于目标值和大于等于目标值的两段,就将问题进行转化成了查找左边界的二分情况
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)
{
left=mid+1;
}
else
{
right=mid;
}
}
if(nums[left]<target)
{
return left+1;
}
return left;
}
};
69、x的平方根
题解
通过阅读题意后,根本没有思路,这时候一定要首先进行考虑暴力的解法,暴力的解法往往是比较容易想到的,并且通过思考暴力解法进行解答,可以为后续的解法提供优化暴力的思路
暴力
既然是求一个数的算数平方根,并且这个数还是非负整数,也就是说这个数的算数平方根肯定是小于这个数的本身的,直接从0到这个数本身进行通过循环进行暴力枚举,找到第一个某个数的平方大于这个数,直接进行返回这个数的前一个数。
二分查找
通过数组的二段性,将问题直接进行转换成查找右边界的二分模板问题
class Solution {
public:
int mySqrt(int x)
{
long long left=0;
long long 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;
}
};
825、山脉数组的顶峰索引
题解
暴力
题目中要求时间复杂度为O(logN),因此通过暴力进行解决本题的想法就失效的,但是可以通过暴力的思想进行将暴力进行优化,暴力每次只能够进行排除一个元素,这时候我们想到是不是可以通过二分进行一次进行多筛选一些元素。
二分查找
既然考虑使用二分进行优化暴力算法,因此需要进行观察需要进行处理的数据是否具有二段性
这道题就完美的诠释了二分算法并不是只有在数组有序的情况下才可以使用,当所处理的元素就有二段性时均可使用。
当二段性开始显现时,就需要进行判断这种情况到底应该使用二分查找的哪种情况,这道题是既可以通过查找左边界进行解决也可以通过查找右边界进行解决,为什么会出现这种情况呢?原因就是看你把峰值归于哪边。
- 把峰值归于数组的左边--查找数组的右端点
class Solution {
public:
int peakIndexInMountainArray(vector<int>& arr)
{
int left=0;
int right=arr.size()-1;
while(left<right)
{
int mid=left+(right-left+1)/2;
if(arr[mid-1]<arr[mid])
{
left=mid;
}
else
{
right=mid-1;
}
}
return left;
}
};
- 把峰值归于数组的右边---查找数组的左端点
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 left;
}
};
162、寻找峰值
题解
这道题和上一道题一摸一样,这里就不在进行分析。
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;
}
};
153、寻找旋转排序数组中的最小值
题解
暴力
通过循环进行遍历整个数组,并通过全局变量min进行更新数组中的最小元素即可,时间复杂度为O(n),不满足上述题目要求的O (logN)。
二分查找
这道题的二分分析是有点东西的,这道题卡了好一会,能够发现数组的二段性,但是找不出来二段性的判断条件
class Solution {
public:
int findMin(vector<int>& nums)
{
int left=0;
int right=nums.size()-1;
int n=nums.size()-1;
while(left<right)
{
int mid=left+(right-left)/2;
if(nums[mid]>nums[n])
{
left=mid+1;
}
else
{
right=mid;
}
}
return nums[left];
}
};
173、点名
题解
暴力
这道题是非常有特点的,在不缺席的情况下,数组元素的下标就是数组中对应位置的值,通过暴力直接进行遍历,查找首个下标和数组中对应的值不同的直接进行返回即可。时间复杂度是O(n).
二分查找
当出现同学进行缺席的情况下,数组中下标对应的位置的值是大于数组下标的,因此数组是具有二段性的,通过二分查找进行优化,这种情况是属于利用二分进行查找左端点的情况,通过二分将时间复杂度优化成O(logN)。
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;
}
}
if(records[left]==left)
{
return left+1;
}
return left;
}
};