二分查找
二分查找也可以看作双指针的一种特殊情况,但我们一般会将二者区分。双指针类型的题,
指针通常是一步一步移动的,而在二分查找里,指针每次移动半个区间长度。
求开方
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;
}