目录
二分查找
1.⼆分查找(easy)
1)朴素二分查找,就是设mid=(left+right)/2,x=nums[mid],t就是我们要找的值
2)二分查找就是要求保证数组有序的前提下才能进行。
3)细节问题:
总结:
细节:
2.二分查找的进阶:
查找区间左端点:
查找区间右端点:
3.在排序数组中查找元素的第一个和最后一个位置
解析:
总结:
总结二分模板:
记忆:
4.搜索插⼊位置(easy)
解析:
5.x的平方根(easy)
解析:
1)暴力:
2)优化:
6.⼭峰数组的峰顶(easy)
解析:
总结:
7.寻找峰值(medium)
解析:
8.搜索旋转排序数组中的最⼩值(medium)
解析:
1)优化:
以D为target:
以A为target:
总结:
9.LCR 点名
解法一:
解法二:
总结一下:
二分查找
二分查找算法,当我们发现一个规律,能在这个规律里面选取一个点之后,能把数组分成两部分,有选择性的选择一部分,进而能在另一部分继续选择,就说明数组有“二段性”。不一定非要数组完全有序,只要能找到一部分有规律,就可以采用二分查找。我们可以找这个数组的二分之一、三分之一、四分之一都行,因为只需要我们能将数组分成两部分就ok。
只要弄清除二分查找算法的进阶,查找"数组"的最左端点 或 查找"数组"的最右端点,对于二分查找的问题一般就迎刃而解了。
那么我们先来看一下朴素的二分查找,后面就来进阶:
1.⼆分查找(easy)
这题就是朴素的二分查找,简直入门级别。了解什么是二分,在什么情况下使用二分。
解析:
1)朴素二分查找,就是设mid=(left+right)/2,x=nums[mid],t就是我们要找的值
1.x<t ,lid-1 ->[left,right];
3.x==t 返回结果
2)二分查找就是要求保证数组有序的前提下才能进行。
3)细节问题:
1.循环结束的条件:left>right,那么while(left<=right)
2.为什么是正确的?
是从暴力解法优化过来的
3.时间复杂度,O(logN)
class Solution {
public:int search(vector<int>& nums, int target) {int n=nums.size();int left=0,right=n-1;while(left<=right){int mid=left+(right-left)/2;if(nums[mid]==target) return mid;else if(nums[mid]<target) left=mid+1;else right=mid-1;}return -1;}
};
总结:
二分查找经典题目。首先最重要的就是判断循环的结束条件,left>right,因为在这之前,left==right的时候,仍然要判断一次nums[left]是否等于target,所以循环条件就是while(left<=right) 然后就是固定的套路
朴素二分查找,就是设mid=(left+right)/2,x=nums[mid],t就是我们要找的值
1.x<t ,left=mid+1 ->[left,right];
2.x>t, right=mid-1 ->[left,right];
3.x==t 返回结果
细节:
为了防止right 和 left 都太大,防止left+right 会存在溢出的风险,所以最好就不要采用这种直接相加的方式。 我们可以先求出这个数组的一半,然后再用这一半长度加上left起始位置,就同样可以得到(left+right)/2 的效果 即:mid=left+(left+right)/2
总结:
二分查找的模板:
while(left<=right)
{
int mid=left+(right-left)/2;
if(nums[mid]==target) return mid;
else if(.....) left=mid+1;
else right=mid-1;
}
根据题目要求,往里面填入相关的条件,最主要的判断条件就是left<=right;二段性。
2.二分查找的进阶:
面对二分查找,那么数组是必须要有序才能进行吗?难道数组不有序,就不能进行二分查找了嘛?
面对这些问题,就有了二分的进阶,可以说学会二分进阶,简直无敌~
我们先来上一个简单示例:
eg:
要我们查早数组某一重复元素的开始和结尾的下标,任何利用O(logN) 的时间复杂度进行查找。
第一肯定会想到暴力,但时间复杂度是O(N) 太大,那么任何利用二分,就可以完美解决这个问题。
所以就想到利用二分的性质,将数组分两块!!!二段性~
一块 < t ; 一块 >= t
那么由此就可以看出,我们要利用二分的性质来查找这一块区间的最左端点和最右端点,就能满足条件,因为这个数组满足二段性,那么如何实现呢?请看下面例子:
查找区间左端点:
1.循环区间:
x<t -> left=mid+1 [left,right]
x>=t -> right=mid [left,right]2.循环条件 left<right
1).如果left==right 就是最终的结果,无需判断
2).如果判断,那么就会进入死循环,因为mid=(left+right)/2,right=mid,就会一直循环3.求中点操作
1).left+(right-left)/2, 不能用left+(right-left+1)/2,因为这样的话,如果数组元素个数是偶数个,那么mid就等之间右边的一个元素,这时,right就跳不到left的位置,一直在mid这个位置进行死循环4.为什么要用right=mid,而不是mid-1;因为我们判断的区间是,在大于等于t这个区间,那么就可能存在等于t的情况,如果冒然写right=mid-1,很可能会跳过nums[right]==t,这个值的区间,所以只能让right=mid
5.为什么判断while(left<=right) 就会死循环?
比如left==right==mid,这三个指针,都指向同一个下标,还要进行判断,那么right就一直等于mid 一直仍然进入循环出不来
查找区间右端点:
1.循环区间:
1)x<=t 那么此时x=nums[mid] 落在区间 [小于等于t] 的位置 那么肯定是更新left=mid,,因为我的left不能越过mid,如果left=mid+1,就会越过mid,如果此时的mid就是指向这个区间的最后一个元素,那么left越过后就到了[mid,right] 这个区间,那么这个区间里面就没有最终的结果
2)x>t 同理就是更新right=mid-1,因为这个区间是确定的,是大于target的值,不会存在我们要找的区间的值,就可以越过mid,成为mid-1
2.循环条件:
left<right,因为跟求左端点一样,如果判断等于的话,left,right,mid三个指针在同一个位置,会陷入死循环3.求中点的方式
1)这次是采用left+(right-left+1)/2,不能采用left+(right-left)/2,因为正好跟求左端点相反,这里求右端点,left==mid,为了防止left一直等于mid陷入死循环,要让mid指向right这一边的中点,所以要让right-left+1
3.在排序数组中查找元素的第一个和最后一个位置
经过上面学习到的二分查找数组区间的左右端点,可以直接试试这题,会有很好的效果,是真的简单。
解析:
1)先将数组分块后,对其求mid,然后判断是求左端点还是求右端点
2)确定好后,对于nums[left] < target -> left=mid+1 的判断,说明left是可以越过mid的。
对于下面的记忆过程可以确定,上面的mid = left+ (right-left)/2 ,是没有+1的。
class Solution {
public:vector<int> searchRange(vector<int>& nums, int target) {//查找区间左端点int n=nums.size();if(n==0) return {-1,-1};int left=0,right=n-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=n-1;while(left<right){int mid=left+(right-left+1)/2;if(nums[mid]>target) right=mid-1;else left=mid;}if(nums[left]==target) ret.push_back(left); else ret.push_back(-1);return ret;}
};
总结:
1)暴力:
从头开始遍历,那时间复杂度肯定O(N^2) 绝对超时2)优化,二分查找左右两端点
1.查找左端点
2.查找右端点
模板
总结二分模板:
查找区间左端点:
while(left<right)
{
int mid=left+(right-left)/2;
if(...) left=mid+1;
else right=mid;
}//查找区间右端点
while(left<right)
{
int mid=left+(right-left+1)/2;
if(...) right=mid-1;
else left=mid;
}
记忆:
当上面出现+1的时候,下面就是right=mid-1
当上面没有+1 的时候,下面就是left=mid+1
4.搜索插⼊位置(easy)
这题和前面一样,这个数据具有二段性,如果这时的你能够看出数组具有二段性,你就真的已经成功了一大步。将数组分为一块小于target;一块大于等于target;
解析:
还是一样的,简单的二分查找,对于返回target元素的下标,或者插入的下标,只要在>= target这个区间内,找到最左边的元素就ok,然后<target 区间的元素就已经确定,那么left=mid+1
class Solution {
public:int searchInsert(vector<int>& nums, int target) {int n=nums.size();int left=0,right=n-1;if(nums[right]<target) return n;if(nums[left]>target) return 0;while(left<right){int mid=left+(right-left)/2;if(nums[mid]<target) left=mid+1;else right=mid;}return right;}
};
学会了之后真的是很简单这种题目,利用二分效率还很高。
5.x的平方根(easy)
这一题,我第一次做的时候,确实没能看出来二段性,用的暴力,后来才发现原来这样是一样具有二段性,就比如从1~x ,那么一块,i*i<x ; 另一块是 i*i>=x;数组分两块,这样就可以照样用暴力。
解析:
1)暴力:
求x的平方根,那么就是找i*i<=x 然后返回i,如果暴力,肯定超时,
2)优化:
可以考虑i~x的值,然后对于i*i 是否小于等于x,求这个区间的最右端点,即利用二分查找求最右端点
考虑到[mid,right] 区间是确定大于x的值,那么right=mid-1,那么上面判断mid就是mid=left+(right-left+1)/2,那么left=mid
class Solution {
public:int mySqrt(int x) {long long left=0,right=x;while(left<right){long long mid=left+(right-left+1)/2;if(mid*mid>x) right=mid-1;else left=mid;}return left;}
};
对于这种二段性数组,掌握了套路简直信手拈来。
6.⼭峰数组的峰顶(easy)
解析:
1)那么还是简单的二分查找模板,只需要简单判断left 或 right会不会越过峰值即可。
class Solution {
public:int peakIndexInMountainArray(vector<int>& arr) {int n=arr.size();int left=0,right=n-1;while(left<right){int mid=left+(right-left)/2;if(arr[mid]<arr[mid+1]) left=mid+1;else right=mid;}return left;}
};
总结:
面对求峰值在图中可以看出 left 是可以越过mid的,成为mid+1,所以这下right=mid,
mid=left+(right-left)/2,就全都一气呵成了~
7.寻找峰值(medium)
解析:
1)寻找数组中的任意一个峰值即可,在取mid的过程中,left和right就又被缩小到一个峰值,那么根上一题一样只需要判断left和right会不会越过峰值,带模板就能解决
class Solution {
public:int findPeakElement(vector<int>& nums) {int n=nums.size();int left=0,right=n-1;while(left<right){int mid=left+(right-left)/2;if(mid<n-1&&nums[mid]<nums[mid+1]) left=mid+1;else right=mid;}return left;}
};
跟上一题一样,就当个练手的,简直easy~
8.搜索旋转排序数组中的最⼩值(medium)
解析:
1)优化:
这题二分查找有点难度,跟之前的不一样,这个在一个区间内都是单调递增的,所以要单独拿出一个数据当作target,就比如D点,A~B段全是大于D的,C~D全是小于或等于D的。那么求这个数组的最小值,就是求C~D的左端点,那么找最左端点就是二分模板,if(nums[mid]>t) left=mid+1;
else right=mid;
if(nums[mid]>t) left=mid+1;
else right=mid; //nums[mid]<=t,这里是C~D段,为了right不越过最小值点,所以right=mid,left在A~B段进行,所以当mid属于最大值点的时候,left也可以越过变成最小值点 所以left=mid+1
以D为target:
class Solution {
public:int findMin(vector<int>& nums) {int n=nums.size();int left=0,right=n-1;int t=nums[n-1];while(left<right){int mid=left+(right-left)/2;if(nums[mid]>t) left=mid+1;else right=mid;}return nums[left];}
};
以A为target:
class Solution {
public:int findMin(vector<int>& nums) {int n=nums.size();int left=0,right=n-1;int t=nums[0];if(n==1) return nums[0];while(left<right){int mid=left+(right-left)/2;if(nums[mid]>=t) left=mid+1;else right=mid;}if(left==n-1&&nums[left]>nums[left-1]) return nums[0];return nums[left];}
};
总结:
这证明了,二分其实是很随意的,但是只要把内在本质掌握好了,不管以哪个点为标准,都可以很快的找到正确答案。
9.LCR 点名
虽然只是一道简单题,但是要是能想到用二分,那么性能又能提升不少~
解法一:
O(N):
1.哈希表
2.直接遍历找结果
3.位运算
4.数学
解法二:
二分查找
仍然是寻找二段性,然后判断left是否可以越过mid
class Solution {
public:int takeAttendance(vector<int>& records) {int n=records.size();int left=0,right=n-1;while(left<right){int mid=left+(right-left)/2;if(mid==records[mid]) left=mid+1;else right=mid;}return left==records[left]?left+1:left;}
};
总结一下:
我觉得二分最主要就是刚开始学习的查找左右两端点,如果了解这些了,那么对于二分算法就是了解的透透的了。再就是对于有序数组,第一就应该要想到双指针和二分查找,如果数组分块,具有二段性,那不用说了,妥妥的二分!
我觉得对我的进步挺大的,希望对你也有帮助~