【Leetcode刷题记录_C++】【二分查找】

本文详细探讨了二分查找在求解平方根、查找数组元素范围、搜索旋转排序数组以及寻找旋转数组中最小值等问题中的应用。通过二分查找算法,实现了O(logn)的时间复杂度解决方案,包括特殊情况的处理,如数组中可能存在重复元素。此外,还涉及了寻找有序数组中位数的高效方法,利用二分法优化查找过程。
摘要由CSDN通过智能技术生成

二分查找

二分查找也可以看作双指针的一种特殊情况,但我们一般会将二者区分。双指针类型的题,
指针通常是一步一步移动的,而在二分查找里,指针每次移动半个区间长度。

求开方

69. x 的平方根

实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

思路:
二分,找到两个相邻平方在目标左右的数

代码:
更快的算法——牛顿迭代法

①
class Solution {
public:
    int mySqrt(int x) {
        if (x < 2)
            return x;
        long l = 0, r = x, a, mq, last=-1;
        while (l < r) {
            a = (l + r) / 2;
            if (last == a)
                return a;
            last = a;
            mq = a * a;
            if (mq > x)
                r = a;
            else if (mq < x)
                l = a;
            else
                return a;
        }
        return a;
    }
};
②
class Solution {
	public:
	int mySqrt(int a) {
		long x = a;
		while (x * x > a) 
			x = (x + a / x) / 2;
		return x;
	}
}

查找区间

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

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

进阶:

你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?

思路:
查找第一次出现:中位比较目标数,如果中位小于等于目标数,那么左端右移一位,继续查找中位,如果中位大,右端变为中位所指位置,直到左端大于右端为止中位等于目标也要缩进左区间,这是为了保证不错过第一次出现
查找最后一次出现:
中位比较目标数,如果中位小于目标数,那么左端右移一位,继续查找中位,如果中位大于等于目标数,右端变为中位所指位置,直到左端大于右端为止,中位数大于目标数才缩进右区间,是为了不错过最后一次出现
(要查找第一次出现,首先要保证每次取到的区间的中位数要尽可能靠近第一次,所以如果中位数小于目标,该数区间之前的就要不得了,就把左指针移动到该数,如果大于等于的话,无法判断是第几个,如果把右指针移到该数,可能会筛掉第一次出现,所以我们就就右指针左移1位再找)
代码:

class Solution {
public:
    int find(vector<int>& nums, int target, int flag) {
        int first = 0, last = nums.size() - 1, mid;
        while (first<last) {
            mid = flag > 0 ? (first + last) / 2 : (first + last + 1) / 2;//难点,保证前后一致
            if (nums[mid] > target)
                last = mid - 1;
            else if (nums[mid] < target)
                first = mid + 1;
            else
                flag>0 ? last-- : first++;
        }
        return first >= nums.size() || nums[first] != target ? -1 : first;
    }
    vector<int> searchRange(vector<int>& nums, int target) {
        if (nums.size() == 0)
            return { -1,-1 };
        return { find(nums, target, 1) ,find(nums, target, -1) };
    }
};

旋转数组查找数字

81. 搜索旋转排序数组 II

已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。

给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。

思路:
对于当前的中点,如果它指向的值小于右端,那么说明右区间是排好序的,然后判断目标是否在右区间,继续二分;反之,断点发生在中点右边,那么说明左区间是排好序的。如果目标值位于排好序的区间内,我们可以对这个区间继续二分查找;反之,我们对于另一半区间继续二分查找。

注意,因为数组存在重复数字,如果中点和左端的数字相同,我们并不能确定是左区间全部相同,还是右区间完全相同。在这种情况下,我们可以简单地将左端点右移一位,然后继续进行二分查找

(旋转数组一般都要用到中位和左右端对比,大于右端,说明左边排好序,小于左端,说明右边排好序,如果等于右端,不好说,这时候右指针不断左移1位进行二分)
代码:

class Solution {
public:
	bool search(vector<int>& nums, int target) {
		int l = 0, r = nums.size() - 1, mid;
		while (l <= r) {
			mid = (l + r) / 2;
			if (nums[mid] == target)
				return true;
			if (nums[mid] > nums[r]) {
				if (nums[mid] > target && nums[l] <= target) 
					r = mid - 1;
				else 
					l = mid + 1;
			}
			else if (nums[mid] < nums[r]) {
				if (nums[mid] < target && nums[r] >= target)
					l = mid + 1;				
				else
					r = mid - 1;
			}
			else {
				if (nums[mid] == nums[l])
					l += 1;
				else
					r = mid - 1;
			}
		}
		return false;
	}
};

练习

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

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:

若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]

注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。

给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

思路:
类似81,利用中点判断哪个区间排好序了,若右区间排好序,说明断点在右区间,不断二分直到区间只剩两个或一个数。
(ps:如果中点与右端相等,在左端右移前,判断左端与中点大小,若左端小,则是左区间排好序,直接输出左端)

(二分找断点)
代码:

int findMin(int* nums, int numsSize){
	int start = 0, end = numsSize - 1, mid;
	while (start < end) {
		if (start + 1 == end || start == end)
			return nums[start] < nums[end] ? nums[start] : nums[end];
		mid = (start + end) / 2;
		if (nums[mid] < nums[end]) 
			end = mid;
		else if (nums[mid] > nums[end]) 
			start = mid;
		else if(nums[start] < nums[mid])
            break;
        else
            start++;
	}
    return nums[start];
}

540. 有序数组中的单一元素

给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
思路:
由题,因为每个数出现两次,只有一个出现一次,我们可以判断在奇偶性上做文章,中点若是当前区间的奇次数,而且他的右边与他相等,则目标数在右区间,以此类推。

(奇偶性,中位和右边的相似性判断断点在左右哪个区间)
代码:

int singleNonDuplicate(int* nums, int numsSize){
	int start = 0, end = numsSize - 1, mid;
	while (start < end - 1) {
		mid = (start + end) / 2;
		if (!((mid-start) % 2)){ //目标数在右区间
            if (nums[mid] == nums[mid + 1])
			    start = mid + 2;
            else
                end = mid;
        }
		else{
            if (nums[mid] != nums[mid - 1])
			    end = mid - 1;
            else
                start = mid + 1;
        }
    }
	return nums[start];
}

4. 寻找两个正序数组的中位数

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数 。
思路:
简单做法类似88,巧一点的方法:因为数组长度已知,合并数组的中位数下标也已知,从两个数组的头开设两个指针,比较大小直到找到中位数下标

我们还可以利用二分来进一步降低时间复杂度,首先要求中位数,因为是有序数组,设总长度为m+n,那么找到比他小的k=(m+n)/2个数即可,那么我们此时对第i个数组做一个分割,割点设为Ci,而LMax= Max(LeftPart),RMin = Min(RightPart)设为割点左右的数,易得LMax1<=RMin1,LMax2<=RMin2,而若LMax1<=RMin2,LMax2<=RMin1,因为有序数组,左边的数小于右边,那么这种情况就是两个数组割点左边的数完全小于总的右边的数,这时如果能使它们割点左边的数总共为k个,那么只要找到割点左右的数就能直接得到中位数了

但是两个数组的最大问题是,它们合并后,m+n总数可能为奇, 也可能为偶,为了避免分情况讨论中位数,所以我们得想法让m+n总是为偶数

通过虚拟加入‘#’,我们让m转换成2m+1 ,n转换成2n+1, 两数之和就变成了2m+2n+2,恒为偶数。

这么虚拟加后,每个位置可以通过/2得到原来元素的位置:

比如 2,原来在0位,现在是1位,1/2=0

比如 3,原来在1位,现在是3位,3/2=1

比如 5,原来在2位,现在是5位,5/2=2

比如 9,原来在3位,现在是7位,7/2=3

而对于割(Cut),如果割在‘#’上等于割在2个元素之间,割在数字上等于把数字划到2个部分,总是有以下成立:

LMaxi = (Ci-1)/2 位置上的元素
RMini = Ci/2 位置上的元素

例如:

割在3上,C = 3,LMax=a[(3-1)/2]=A[1],RMin=a[3/2] =A[1],刚好都是3的位置!

割在4/7之间‘#’,C = 4,LMax=A[(4-1)/2]=A[1]=4 ,RMin=A[4/2]=A[2]=7

把2个数组看做一个虚拟的数组A,A有2m+2n+2个元素,割在m+n+1处,所以我们只需找到m+n+1位置的元素和m+n+2位置的元素就行了。

左边:A[m+n+1] = Max(LMax1,LMax2)

右边:A[m+n+2] = Min(RMin1,RMin2)

==>Mid = (A[m+n+1]+A[m+n+2])/2 = (Max(LMax1,LMax2) + Min(RMin1,RMin2) )/2

最快的割(Cut)是使用二分法,

有2个数组,我们对哪个做二分呢?
根据之前的分析,我们知道了,只要C1或C2确定,另外一个也就确定了。这里,为了效率,我们肯定是选长度较短的做二分,假设为C1。

LMax1>RMin2,把C1减小,C2增大。—> C1向左二分

LMax2>RMin1,把C1增大,C2减小。—> C1向右二分

如果C1或C2已经到头了怎么办?

这种情况出现在:如果有个数组完全小于或大于中值。假定n<m, 可能有4种情况:

C1 = 0 —— 数组1整体都在右边了,所以都比中值大,中值在数组2中,简单的说就是数组1割后的左边是空了,所以我们可以假定LMax1 = INT_MIN

C1 =2n —— 数组1整体都在左边了,所以都比中值小,中值在数组2中 ,简单的说就是数组1割后的右边是空了,所以我们可以假定RMin1= INT_MAX,来保证LMax2<RMin1恒成立

C2 = 0 —— 数组2整体在右边了,所以都比中值大,中值在数组1中 ,简单的说就是数组2割后的左边是空了,所以我们可以假定LMax2 = INT_MIN

C2 = 2m —— 数组2整体在左边了,所以都比中值小,中值在数组1中, 简单的说就是数组2割后的右边是空了,为了让LMax1 < RMin2 恒成立,我们可以假定RMin2 = INT_MAX

(既然是中位数,必然是两个数组合并后的第(m+n)/2位左右的数,为此呢,我们可以有一种便捷的方法,记k为(m+n)/2,分别找到两个数组k/2处,设这个分界线两边的数分别为LMax1,LMax2,RMin1,RMin2,这样就总有一个总数为k的区间,为使得我们框住的区间的数是小于等于中位数的,就要保证LMax1,LMax2都小于RMin1,RMin2,如果不满足,就移动两个指针,但是现在求中位数还要分辨单双数,为方便每个数后插入一个#号,使得它总长一定成为偶数,然后如果割在‘#’上等于割在2个元素之间,割在数字上等于把数字划到2个部分,后面就按照正常进行就好)
代码:

double findMedianSortedArrays(int* nums1, int nums1Size, int* nums2, int nums2Size) {
    int n = nums1Size; int m = nums2Size;
    if (n > m) //保证数组n一定最短 
        return findMedianSortedArrays(nums2, nums2Size, nums1, nums1Size);
    int LMax1, LMax2, RMin1, RMin2; //LMax1和Rmin1 表示数组1在切割之后左边的最大值和右边的最小值,同理LMax2和RMin2
    int c1, c2;//c1表示数组1切割的位置,同理c2
    int lo = 0, hi = 2 * n;  //我们目前是虚拟加了'#'所以数组1是2*n长度  
    while (lo <= hi)   //二分
    {
        c1 = (lo + hi) / 2;  //c1是二分的结果
        c2 = m + n - c1;

        LMax1 = (c1 == 0) ? INT_MIN : nums1[(c1 - 1) / 2]; //左空:c1为0,说明左边是空的,LMAX1=INT_MIN
        RMin1 = (c1 == 2 * n) ? INT_MAX : nums1[c1 / 2];   //右空:c1为2n,说明右边是空的,RMin1 = INT_MAX
        LMax2 = (c2 == 0) ? INT_MIN : nums2[(c2 - 1) / 2];
        RMin2 = (c2 == 2 * m) ? INT_MAX : nums2[c2 / 2];

        if (LMax1 > RMin2)
            hi = c1 - 1;           //LMax1值大于了RMin2,所以c1要往左移1位,在左半取找,更新上限hi=c1-1;
        else if (LMax2 > RMin1)
            lo = c1 + 1;           //Lmax2值大于RMin1,所以c1要右移1位,在又半区找,更新下限lo=c1+1;
        else
            break;                 //当同时满足 LMAX1<= RMin2 && LMAX2 <= RMin1的时候就找到了,退出循环
    }
    return (fmax(LMax1, LMax2) + fmin(RMin1, RMin2)) / 2.0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ycr的帐号

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值