基础算法:二分查找
目录
一、二分查找
二、在排序数组中查找元素的第一个和最后一个位置
三、x的平方根
四、搜索插入位置
五、山脉数组的峰顶索引
六、寻找峰值
七、寻找旋转排序数组中的最小值
八、点名
二分查找本质是利用数据的二段性,将数据划分为两个区间,然后舍弃一部分区间,高效地找到目标点。
一、二分查找
704. 二分查找 - 力扣(LeetCode)
哎,本以为自己能自信写过,没想到写出了死循环。
更新边界应该舍弃mid,取向mid相邻的位置。
循环条件left==right时仍然要判断,所以循环条件不能只写left
理论上能将一段数据分为两段,通过判断舍弃一部分数据,只关注另一部分数据的思路都是二分。
class Solution {
public:int search(vector<int>& nums, int target) {int left=0,right=nums.size()-1,mid;while(left<=right){mid=left+(right-left)/2;if(nums[mid]<target){left=mid+1;}else if(nums[mid]>target){right=mid-1;}else{//直接return就行了,别用更新ansreturn mid;}}return -1;}
};
二、在排序数组中查找元素的第一个和最后一个位置
34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
查找左端点:
思路就是最左边的那个坐标,左边恰好全是小于它的,这也是题目要我们实现的,如何实现?从它开始的右边都是大于等于它的,循环条件left<right,终止时left==right==目标点位,mid记录是有left+(right-left)/2,和left+(right-left+1)/2,两种,区别在于仅当数组大小为偶数时,前者往前取整,后者往后取整,具体用那种要看是谁越过边界(代码里就是谁舍弃mid),left越过边界就是往前取整(left越过边界抵达目标,再等待right赋值mid到达目标点,如果用向后取整,right先抵达目标就会死循环。)(最后left,right相邻,向后取整会让right一直赋值mid,死循环),right就是向后取整。这里得用前者,用后者会和x>=t,right=mid引发死循环,判断时让left=mid+1无限逼近它后+1越过那个小于的区间!就到了右边第一个位置,再等待mid不断往前取整,right最终停留在该位置。
记x=【mid】,x<target,left=mid+1,x>=target,right=mid,判断条件
left<right,退出循环时left==right==左下标。
查找右端点思路可以模仿这个思考。
class Solution {
public:vector<int> searchRange(vector<int>& nums, int target) {if(nums.empty())return{-1,-1};int left=0,right=nums.size()-1,mid,begin=-1,end=-1;while(left<right){//向左取,左边界舍弃mid//【mid】<target,left舍弃mid,更新为mid+1//else 就让right=mid,因为不断向左取,当left到达目标点便不再更新//而right不断赋值mid,逼近目标点位,最终到达目标点右边相邻点位,下一次mid向左取->抵达目标点mid=left+(right-left)/2;if(nums[mid]<target){left=mid+1;}else{right=mid;}}if(nums[left]==target)begin=left;right=nums.size()-1;while(left<right){//向右取,右边界舍弃mid//【mid】>target,right舍弃mid,更新为mid-1//else 就让left=mid,因为不断向右取,当right到达目标点便不再更新//而left不断赋值mid,逼近目标点位,最终到达目标点左边边相邻点位,下一次mid向右边取->抵达目标点mid=left+(right-left+1)/2;if(nums[mid]<=target){left=mid;}else{right=mid-1;}}if(nums[left]==target)end=left;return {begin,end};}
};
三、x的平方根
69. x 的平方根 - 力扣(LeetCode)
根据第二题的分析,这题要舍弃小数部分,其实就是找右端点,保证右端点的右边恰好全部都是大于它的,这个目标点就是我们要求的点。
那套模板就行了,另外因为mid要做乘的操作,所以都开long long
class Solution {
public:int mySqrt(int x) {//mid后续要相乘开longlong,又mid需要left和right赋值,LR都开longlonglong long left = 0, right = x,mid;while (left < right) // 相等时就是位置{mid = left + (right - left + 1) / 2;if (mid * mid <= x) {left = mid;} else {right = mid - 1;}}return left;}
};
四、搜索插入位置
35. 搜索插入位置 - 力扣(LeetCode)
很明显是找左边界的题(保证右边恰好全是小于目标点的)
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;}}return nums[left]<target?left+1:left;}
};
或者按照最朴素的二分来做即可,循环结束是必然left=right+1,此时left指向第一个大于等于target的位置
class Solution {
public:int searchInsert(vector<int>& nums, int target) {int left=0,right=nums.size()-1,mid;while(left<=right){mid=left+(right-left)/2;if(nums[mid]<target){left=mid+1;}else if(nums[mid]>target){right=mid-1;}else{return mid;}}//插入在头还是尾,left都能解决return left;}
};
五、山脉数组的峰顶索引
852. 山脉数组的峰顶索引 - 力扣(LeetCode)
由于数据二段性,不难想到二分算法,难点在于更新边界的判断条件
数据分为两部分,前部分单调递增,后部分单调递减,只需判断mid及其相邻一个单位的数据就可以实现left和right更新。
这里就是找右边界,因为按照题意不可能出现等于的情况,所以更新left判断的时候我把等于删了,你想写也可以,nonono仔细想想发现这里也不是什么边界问题,思想似乎有点被固化了,这里left和right的更新根据判断条件的mid和mid-1/+1来判断即可,看条件下哪个是我们要的点,让left和right都不断向目标点逼近。然后你根据一下你自己的mid的更新条件(取左还是右),来选择是使用mid-1还是mid+1,mid取左的话避免死循环就是right赋值mid,那么left=mid+1用mid+1当判断条件,mid取右的话避免死循环就是left赋值mid,那么right=mid-1用mid-1来当判断条件。
基于此,峰顶目标点就是找右边界,此时右边恰好开始下降
不得不感叹,真是,妙妙妙妙哇( ̄y▽ ̄)╭ Ohohoho.....
class Solution {
public:int peakIndexInMountainArray(vector<int>& arr) {int left=0,right=arr.size()-1,mid;//不写等于就是有一边要越界的,相等时就是位置while(left<right){//+1因为后边是left赋值mid,防止在left处死循环mid=left+(right-left+1)/2;//递增,mid是我们要的点if(arr[mid-1]<arr[mid]){left=mid;}//递减,mid-1是我们要的点else{right=mid-1;}}return left;}
};
六、寻找峰值
162. 寻找峰值 - 力扣(LeetCode)
存在多个峰值,我们找一个即可,那题目很宽容了
思路和上一题一样,可以参考上一题
class Solution {
public:int findPeakElement(vector<int>& nums) {int left=0,right=nums.size()-1,mid;while(left<right){mid=left+(right-left)/2;//递减,mid是我们要的点if(nums[mid]>nums[mid+1]){right=mid;}//递增,mid+1是我们想要的点else{left=mid+1;}}return left;}
};
七、寻找旋转排序数组中的最小值
153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)
很明显,数据具有二段性,且题目要求复杂度logn,于是很容易想到二分,观察到两段数据都是递增的,自然不能用比较mid相邻元素来更新区间,画图后发现,如果旋转了,那么前半段的数值一定大于数组末尾元素,后半段元素则全部小于等于元素,于是可以利用这个来更新左右边界。
然后这里>的时候mid+1才是我们想要的,更新的是left,那么避免死循环所以mid更新是向左取
class Solution {
public:int findMin(vector<int>& nums) {int n=nums.size();int left=0,right=n-1,mid;while(left<right){mid=left+(right-left)/2;if(nums[mid]>nums[n-1]){left=mid+1;}else{right=mid;}}return nums[left];}
};
八、点名
LCR 173. 点名 - 力扣(LeetCode)
聪明如你一眼就看出怎么写了
class Solution {
public:int takeAttendance(vector<int>& records) {if(records.size()==1){if(records[0])return 0;return 1;}int n=records.size(),left=0,right=n-1,mid;while(left<right){mid=left+(right-left)/2;if(records[mid]==mid){left=mid+1;}else{right=mid;}}//left左边恰好全是+1递增,那么left位置可能是仍然+1//也可能是跳过了这个+1,利用下标和数组元素相等特性进行判断即可//你想取右边界最终也是这样返回return left==records[left]?left+1:left;}
};
此篇完。