1. 二分查找简介
关于二分查找,想必有很多同学都是有所耳闻的,我们在C语言阶段也接触过,当时我们说的是数组有序的情况下才可以使用二分查找,但是在这里,这句话是有局限性的,在一些特定情况下,无序的也可以用二分来解决。二分查找算法存在3个模板,分别是最简单版本、查找左边界、查找右边界,下面将通过例题为大家一一展现。
2. 二分查找
题目展示:
题目分析:
本题属于最简单的二分算法,要用到上面提到的第一种模板。
大家可以看上图,要注意的首先是循环结束的条件,一定是left<=right,因为相等的情况下,就剩一个数,这个数仍然需要判断;其次,二分算法的时间复杂度是O(logN),这意味着其效率是非常高的。
代码实现:
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+1)/2;
if(nums[mid]<target)
{
left=mid+1;
}
if(nums[mid]>target)
{
right=mid-1;
}
if(nums[mid]==target)
{
return mid;
}
}
return -1;
}
};
朴素模板展示:
大家能够发现,这个模板其实非常简单,省略号中的内容需要根据具体题目分析出"二段性"才可以得到,这里的"二段性"是解决问题的关键。
3. 在排序数组中查找元素的第一个位置和最后一个位置
题目链接:34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
题目展示:
题目分析: 这道题用上一道题的方法就无法解决了,因为我们无法确定找到的mid是否为边界点,而本题要求的是找到左端点和右端点,所以本题要运用前面说过的后两种模板来解决,具体大家来看下图;
大家注意与朴素二分进行区分,以查找左端点为例,我们将数组分为了两个区间,这也就是前面所说的二段性,在左端点左边,一定是小于目标值的,这时指针的移动和前面是一样的;而左端点右边的部分,是大于或者等于目标值的,这时我们指针的移动就需要与前面进行区别了,为什么会改变呢?
大家可以来思考这样一种情况,当我们的mid指向左端点时,那么此时,如果按照之前朴素二分的方法去写,即right=mid-1,这时我们就会错过左端点,导致找不到左端点了,所以指针的移动方式是right=mid。同理,查找右端点时也是一样的分析方法。
还有两点细节需要大家注意,循环条件和求中点的方式,循环条件一定是left<right,这与朴素解法是有明显区别的,具体原因大家可以参照上图中的解释;求中点的方式对于两种不同的查找方式也有所区别,在朴素二分中,这两种求中点的方式都是OK的,但是在这里是需要匹配使用,否则会导致死循环。
代码实现:
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;
int begin=0;
//二分左端点
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;//标记左端点
}
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};
}
};
模板总结:
上面就是两种二分方法的模板,大家可以根据对原理的理解去记忆。
4. x的算术平方根
题目链接:69. x 的平方根 - 力扣(LeetCode)
题目展示:
题目分析:本题要求我们求一个数的算数平方根,不能用内置函数,这道题我们怎么去寻找二段性呢?大家来看下图:
大家可以发现,我们最终的结果的平方一定在上图所示的两个区间的其中之一里,所以二段性就出来了,这时我们仅需要对不同的情况进行分类讨论就可以解决问题了。
代码实现:
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 left;
}
};
5. 搜索插入位置
题目链接:35. 搜索插入位置 - 力扣(LeetCode)
题目展示:
题目分析:
代码实现:
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;
}
};
6. 山脉数组的封顶索引
题目链接:852. 山脉数组的峰顶索引 - 力扣(LeetCode)
题目展示:
题目分析:本题需要我们找到顶峰元素的下标,我们需要找到题目中存在的二段性,具体大家来看下图:
大家可以看到,以顶峰元素为界,我们将数组分成了两部分,两部分各自有特殊的规律,就得到了二段性。
代码实现:
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]>arr[mid-1])
{
left=mid;
}
if(arr[mid]<arr[mid-1])
{
right=mid-1;
}
}
return left;
}
};
7. 寻找峰值
题目展示:
题目分析:本题与上一题有一些类似,也是让我们寻找峰值并返回其下标,分析方法类似;
这里大家要注意本题二段性的由来,以mid为界,当mid右边是递减序列时,意味着左边一定会出现峰值,因为最左边是负无穷;同理,当mid左边是递增序列时,意味着右边一定会出现峰值,因为最右边也是负无穷。由此,二段性就出来了。
代码实现:
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;
}
if(nums[mid]>nums[mid+1])
{
right=mid;
}
}
return left;
}
};
8. 寻找旋转排序数组中的最小值
题目链接:153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)
题目展示:
题目分析: 本题要求我们设计算法复杂度为O(logN),其实就已经暗示我们使用二分来解决;
这里大家可以看到,我们根据数组的特性,可以将其分成两部分,在图中可以发现,AB段的值一定大于D点的值,而CD段的值一定小于或者等于D点的值;当mid落在AB段时,意味着结果肯定不会在mid左边;同理当mid落在CD段时,意味着结果肯定不会在mid右边。
代码实现:
class Solution {
public:
int findMin(vector<int>& nums) {
int left=0;
int right=nums.size()-1;
int x=nums[right];
while(left<right)
{
int mid=left+(right-left)/2;
if(nums[mid]<=x)
{
right=mid;
}
if(nums[mid]>x)
{
left=mid+1;
}
}
return nums[left];
}
};
9. 点名问题
题目链接:LCR 173. 点名 - 力扣(LeetCode)
题目展示:
题目分析:本题有很多种解法,这里我们只介绍二分解法,而二分解法也是本题的最优解法;
大家可以看到本题的二段性来源于数组中的值与其下标的对应关系,以消失的那个数字为界,我们可以将数组分为两部分,左边是数组值与下标相等的情况,右边是数组值与下标不等的情况;由此我们就可以得到二段性,当mid位于左边区间时,那么结果就一定不在mid左边;同理,当mid位于右边区间时,那么结果就一定不在mid右边。
代码实现:
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;
}
};
10. 总结
本篇博客为大家介绍了二分算法,其核心在于寻找二段性,二段性确定了,代码可以直接套模板进行编写。二分算法是一种效率很高的算法,可以帮助我们解决一些比较复杂的问题,希望大家可以掌握上面所提到的例题,最后希望本篇博客可以为大家带来帮助,感谢阅读!