二分法题解合集以及模板【第一部分】

一遍就绿给人惊喜,但红色才是coding的常态

T0 好用模板

自己平时写二分全靠运气,运气好的时候就能水过二分,究其原因还是自己对二分的过程不了解清楚,因此专门写一个BLOG来记录刷爆二分的过程。

首先贴一个究极好用模板:https://www.pianshen.com/article/90471307044/
二分法求解寻找有序序列中“第一个”满足某条件的元素的位置的模板:

//二分区间为左闭右闭的[left,right]
//解决“寻找有序序列第一个满足某条件A的元素的位置”
int solve(int left,int right){
	int mid;
	while(left<right){
		mid=(left+right)>>1;
		if(条件A成立){
			right=mid;
		}
		else left=right+1;
	}
	return left;
}

T1 二分查找

P704二分查找
在这里插入图片描述
我们考虑条件A是什么,题目要找到等于target的目标位置,因此我们查找的条件为:有序序列中第一个大于等于target的位置,如果这个位置对应的数是target,就说明找到了,如果不是,则不存在这个数,返回-1。代码如下:

int search(vector<int>& nums, int target) {
    int n=nums.size();
    int left=0,right=n-1;
    int mid;
    while(left<right){
        mid=(left+right)>>1;
        if(nums[mid]>=target) right=mid;
        else left=mid+1;
    }
    if(nums[left]==target) return left;
    else return -1;
}

T2 在排序数组中查找元素的第一个和最后一个位置

P34在排序数组中查找元素的第一个和最后一个位置
在这里插入图片描述

本题可以分成两个二分来实现,第一个二分条件A为:寻找有序序列中第一个大于等于target的数的位置,查找到的第一个数如果不等于target,就说明数组中不存在target这个数,直接返回[-1,-1],如果等于target的话,起始位置就是查找到的这个位置,再进行第二次二分查找,条件A未寻找有序数列中第一个大于target的位置,注意,如果不存在大于target的数,即target是最大的数,那么返回的值left是会停在n-1的位置的,因此特判以下即可,代码如下:

vector<int> searchRange(vector<int>& nums, int target) {
    int n=nums.size();
    vector<int>ans(2,-1);
    if(n==0) return ans;
    int left=0,right=n-1;
    int mid;
    while(left<right){
        mid=(left+right)>>1;
        if(nums[mid]>=target) right=mid;
        else left=mid+1;
    }
    if(nums[left]==target) ans[0]=left;
    else return ans;
    left=0,right=n-1;
    while(left<right){
        mid=(left+right)>>1;
        if(nums[mid]>target) right=mid;
        else left=mid+1;
    }
    if(nums[left]==target) ans[1]=left; //特判一下target是否是最大的数
    else  ans[1]=left-1;
    return ans;
}

T3 搜索旋转排序数组

P33搜索旋转排序数组
在这里插入图片描述
本题要求即几乎有序的数组,查找某个元素是否存在问题,因此我们可以把数组一分为二,则一定有一段是完全有序,而另一段是基本有序,我们在完全有序的数组中二分查找,没找到就继续分基本有序的数组,直到left=right为止。代码如下:

int search(vector<int>& nums, int target) {
    int n=nums.size();
    int left=0,right=n-1;
    int mid;
    while(left<right){
        mid=(left+right)>>1;
        if(nums[mid]==target) return mid;
        else if(nums[mid]<nums[right]){ //说明有序数组是[mid,right],进行二分查找
            if(nums[mid]>=target||nums[right]<target) right=mid; //注意条件
            else left=mid+1;
        }
        else{ //有序数组是[left,mid]
            if(nums[mid]>=target&&nums[left]<=target) right=mid;
            else left=mid+1;
        }
    }
    return nums[left]==target? left:-1;
}

写二分不能全靠模板,还是要真正自己理解


T4 寻找旋转排序数组中的最小值 II

P154寻找旋转排序数组中的最小值 II
在这里插入图片描述
我们讨论在一个包含重复元素的升序序列中,在区间[left,right]之间搜索整个序列的最小值的情况:令mid=(left+right)/2;

  1. nums[mid]<nums[right],说明可以忽略右边的元素,因此right=mid
    在这里插入图片描述

  2. nums[mid]>nums[right],说明可以忽略左半部分元素,因此left=mid+1;

    在这里插入图片描述

  3. nums[mid]==nums[right],不能轻易判断哪一边可以忽略,
    但一定有nums[right]==nums[right-1],因此让right-1;

    因此,容易得到代码如下:

int findMin(vector<int>& nums) {
    int n=nums.size();
    int left=0;
    int right=n-1;
    int mid;
    while(left<right){
        mid=(left+right)>>1;
        if(nums[right]>nums[mid]){
            right=mid;
        }
        else if(nums[right]<nums[mid]){
            left=mid+1;
        }
        else right--;
    }
    return nums[left];
}

T5 山脉数组的峰顶索引

P852山脉数组的峰顶索引
在这里插入图片描述
题目即要求是:在一个先严格递增再严格递减的数组中,求最大值的位置。很明显可以用二分法,通过对最大值的分析,记满足要求的最大值的下标满足以下性质: i a n s i_{ans} ians,则:

  • i i i < i a n s i_{ans} ians时,arr[i]>arr[i-1]恒成立
  • i i i > i a n s i_{ans} ians时,arr[i]<arr[i-1]恒成立

则题目转换为:寻找第一个满足arr[i]<arr[i-1]的位置i,最后结果即为i-1

int peakIndexInMountainArray(vector<int>& arr) {
    int n=arr.size();
    int mid;
    int left=0;
    int right=n-1;
    while(left<right){
        mid=(left+right)>>1;
        if(arr[mid]<arr[mid-1]) right=mid;
        else left=mid+1;
    }
    return left-1;
}

T6 山脉数组中查找目标值

P1095山脉数组中查找目标值
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
从T5我们可以直到,山脉数组就是先严格单调递增再严格单调递减的一个数组,并且T5我们已经学会了求山脉数组的最高点,因此我们可以先求出山脉数组的最高点,然后再分别在两段单调有序的数组中求出第一个大于等于target的数和第一个小于等于target的数并判断是否等于target,就如果有一个等于就直接返回数组下标,都不等于就直接返回-1,相当于是T5求山脉数组的顶峰索引值于T1二分查找的结合,求三次二分。如果把前几个题掌握了这个题应该是不难的。代码如下:

int findInMountainArray(int target, MountainArray &mountainArr) {
    int mid;
    int left=0,right=mountainArr.length()-1;
    while(left<right){ //求山脉数组的顶峰索引值
        mid=(left+right)>>1;
        if(mountainArr.get(mid)<mountainArr.get(mid-1)){
            right=mid;
        }
        else left=mid+1;
    }
    int high=left-1; //顶峰索引值high
    left=0,right=high;
    while(left<right){ //在顶峰左边搜索target
        mid=(left+right)>>1;
        if(mountainArr.get(mid)>=target) right=mid;
        else left=mid+1;
    }
    if(mountainArr.get(left)==target) return left; //搜索到返回索引
    else{
        left=high,right=mountainArr.length()-1;
        while(left<right){ //在顶峰右边搜索target
            mid=(left+right)>>1;
            if(mountainArr.get(mid)<=target) right=mid;
            else left=mid+1;
        }
        if(mountainArr.get(left)==target) return left;
        else return -1; //都没搜索到,返回-1
    }
}

T7 寻找两个正序数组的中位数(难题)

P4寻找两个正序数组的中位数
在这里插入图片描述
O(m+n)的算法很好做,但我们需要在O(log(m+n))的时间复杂度下完成,就需要用到二分的方法来做。本题具有一定的难度
首先我们考虑中位数的定义:当两个数组长度和m+n是奇数时,中位数是合并数组之后的(m+n)/2个元素,当m+n是偶数时,中位数是合并数组之后的(m+n)/2和(m+n)/2+1的平均值。即本题可以转化为:求两个正序数组中的第k小的数,其中k为m+n)/2或(m+n)/2+1。
假定两个有序数组分别为A和B,我们考虑A[k/2-1]和B[k/2-1],他们前面分别有A[0]–A[k/2-2],即k/2-1个元素,**对于A[k/2-1]和B[k/2-1]中的较小值,在合并数组中最多只有(k/2-1)+(k/2-1)=k-2个元素,**因此他必定不是第k小元素。
我们可以总结出三种情况:

  • 如果A[k/2-1]<B[k/2-1],那么比A[k/2-1]小的数最多只有k-2个,即A[0]到A[k/2-1]都不可能是合并数组后第k小的数
  • 如果A[k/2-1]>B[k/2-1],那么比B[k/2-1]小的数最多只有k-2个,可以排除B[0]到B[k/2-1]。
  • 如果A[k/2-1]=B[k/2-1],可以归入第一种情况处理。

可以看到,比较A[k/2−1]和B[k/2−1] 之后,可以排除 k/2个不可能是第k小的数,查找范围缩小了一半。同时,我们将在排除后的新数组上继续进行二分查找,并且根据我们排除数的个数,减少 k的值,这是因为我们排除的数都不大于第k小的数。
有以下的三种情况需要特别处理:

  • 如果A[k/2-1]或者B[k/2-1]越界,那么我们选取对应数组的最后一个元素,并根据排除数的个数减少k的值,而不能直接将k减去k/2。
  • 如果一个数组为空,说明该数组所有元素都被排除,我们可以直接返回另一个数组中的第k小元素。
  • 如果k=1,直接返回两个数组中的元素的最小值即可。
int getEle(const vector<int>& nums1,const vector<int>& nums2,int k){
    int m=nums1.size();
    int n=nums2.size();
    int index1=0,index2=0;
    while(true){
    	//特殊情况
        if(index1==m) return nums2[index2+k-1];
        if(index2==n) return nums1[index1+k-1];
        if(k==1) return min(nums1[index1],nums2[index2]);
        //一般情况
        int newIndex1=min(index1+k/2-1,m-1);
        int newIndex2=min(index2+k/2-1,n-1);
        int pivot1=nums1[newIndex1];
        int pivot2=nums2[newIndex2];
        if(pivot1<pivot2){
            k-=newIndex1-index1+1;
            index1=newIndex1+1;
        }
        else{
            k-=newIndex2-index2+1;
            index2=newIndex2+1;
        }
    }

}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
    int m=nums1.size();
    int n=nums2.size();
    int totalsize=m+n;
    if(totalsize%2==1) return getEle(nums1,nums2,(totalsize+1)/2);
    else return (getEle(nums1,nums2,totalsize/2)+getEle(nums1,nums2,totalsize/2+1))/2.0;
}

T8 找到 K 个最接近的元素

P658找到 K 个最接近的元素
在这里插入图片描述
原本数组就是有序的,我们可以充分利用这一点:

  • 如果目标值x小于等于有序数组的第一个元素,那么答案就是有序数组的前k个数
  • 如果目标值x大于等于有序数组的最后一个元素,那么答案就是最后k个数
  • 其他情况,我们考虑用二分法找到最接近x的有序数组中的元素下标index,然后利用双指针的思想,令左指针等于index-1,右指针等于index+1,考虑开区间(l,r),在不溢出的前提下,当arr[l]与x的差值小于等于arr[r]与x的插值时,左指针左移(一定是小于等于,因为当左右指针指的数和x的差值相同时,一定是左移左指针),否则右指针右移。当一个指针移到头时,直接移动另一个指针,直到开区间里面的数有k个为止。代码如下:
vector<int> findClosestElements(vector<int>& arr, int k, int x) {
    int n=arr.size();
    int left=0,right=n-1;
    int mid;
    if(x<=arr[0]) return vector<int>(arr.begin(),arr.begin()+k); //直接返回前k个
    if(x>=arr[n-1]) return vector<int>(arr.end()-k,arr.end()); //直接返回后k个
    while(left<right){ //找到第一个大于等于x的数的下标
        mid=(left+right)>>1;
        if(arr[mid]>=x) right=mid;
        else left=mid+1;
    }
    int index=((arr[left]-x)<(x-arr[left-1])? left:left-1);//最接近x的数的下标只可能是left或left-1
    int l=index-1,r=index+1; //开区间(l,r)
    k--; //第一个数包含
    while(k){ //直到开区间内的个数等于k
        k--;
        if(l<0){ //左指针到头了
            r++;
            continue;
        }
        if(r==n){ //右指针到头了
            l--;
            continue;
        }
        if((x-arr[l])<=(arr[r]-x)&&l>=0&&r<n) l--; //注意一定要取等于
        else if((x-arr[l])>(arr[r]-x)&&l>=0&&r<n) r++;
    }
    return vector<int>(arr.begin()+l+1,arr.begin()+r); //返回开区间(l,r)内的数
}

时间复杂度O(log(n)+k),空间复杂度O(1)。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值