文章目录
1、搜索旋转排序数组&在排序数组中查找元素的第一个和最后一个位置
题目
分析
这题很明显是需要使用二分法的,这里顺便将二分法的使用做下总结。
①目标明确
使用二分法的步骤不外乎找到左右端点,然后取中间端点,再根据情况将左或右端点变成中间端点。这里一定要先理清什么情况将左端点变为中间端点,什么情况将右端点变为中间端点。
②另一种思路:排除法
这里是对mid划分的标准改一下,划分后一部分一定不存在目标元素,而另一部分可能存在目标元素(这里这样做是因为有的题目可能并不一定是要你找某个特定的元素)。如下图所示
使用上述解法,最后left是符合标准的最后一位的解(见题目:在排序数组中查找元素的第一个和最后一个位置)。
同理,如果是找第一个位置就可反过来,让mid落在left,同时每次迭代让left+1.
③避免死循环
如上图3(2)中所示,在循环至最后两个数时可能会发生死循环。
之所以会死循环,是因为,条件使得mid一直等于left,而left也不会向right那样写成mid+1,因为这里我们是将区域严格的分为了[left,mid-1]和[mid,right]两个部分,如果同时写left=mid+1,right=mid-1的话就会把mid给pass掉。
这里的解决方案是,令
mid=left+(right-left+1)/2
这样就能使mid每次都向上去整。
这样做的另一个好处是我们刻意使left向目标数上靠拢,最后的输出值可以直接选left。
总结
这里以33为例:
第一步,找mid,注意这里把区间分成了[left,mid-1]和[mid,right]两部分,这里除非left==right(事实上因为while的条件是left<right,所以这种情况不会发生),mid的取值永远是在left的右边。
int mid = left + ((right - left + 1)>>1);
第二步,判断是在mid和right是否在旋转点的同一侧,代码如下:
int search(vector<int>& nums, int target) {
int n=nums.size();
if(n==0) return -1;
int left=0,right=n-1;
while(left<right){
int mid=left+((right-left+1)>>1);
//当满足下面情况时,mid和right均在旋转点的同一侧,或者他们直接就都位于旋转点上面
if(nums[mid]<=nums[right]){
//满足下面情况,表示目标点在[mid,right]的范围内
if(target>=nums[mid]&&target<=nums[right]) left=mid;
else right=mid-1;
}
//mid和right是否在旋转点的异侧
else{
//满足下面情况,表示目标点在[left,mid)的范围内
if(target<nums[mid]&&target>=nums[left]) right=mid-1;
else left=mid;
}
}
return (nums[left]==target)?left:-1;
}
复杂度
时间复杂度:O(lgN)
空间复杂度:O(1)
2、旋转数组的最小数字
题目
分析
参考:link
这题可以对比下 4 中的第33题。
这里使用二分法来处理,和33题一样,这题的关键在于判断当前mid处于哪半部分。
具体操作如下:
1)当 numbers[m] > numbers[j]时: m 一定在 左排序数组 中,即旋转点 x 一定在 [m + 1, j] 闭区间内,因此执行 i = m + 1。
2)当 numbers[m] < numbers[j] 时:m 一定在 右排序数组 中,即旋转点 x 一定在 [i, m] 闭区间内,因此执行 j = m。
3)当 numbers[m] == numbers[j] 时: 无法判断 m在哪个排序数组中,即无法判断旋转点 x 在 [i, m]还是 [m + 1, j] 区间中。
解决方案: 执行 j = j - 1缩小判断范围。
前两种情况好说,这里关键在于情况3。
另一点要注意的是,这里不能像33题那样,使用
while (left < right) {
int mid = left + ((right - left+1 )>>1);
if (numbers[mid] > numbers[right]) {
left = mid;
}
else if (numbers[mid] < numbers[right]) right = mid-1;
else --right;
}
的迭代形式,关键还是在于第三情况。如旋转矩阵是 { 10,10,10,0,10 },
在第一轮循环过后,right就指向了目标值。
而后面就会一直是left = mid直到left=right-1,而此时会导致 --right ,从而导致错误。
即如果这样写,在这种情况下,最后肯定会导致right指向目标值,left=right-1,mid=right。
所以正确的写法应该是:
while(left<right){
int mid = left + ((right - left)>>1);
if(numbers[mid]>numbers[right]){
left=mid+1;
}
else if(numbers[mid]<numbers[right]) right=mid;
else --right;
}
复杂度
时间复杂度:O(lgN)
空间复杂度:O(1)
3、寻找重复数
题目
分析
参考:link
这题是道要求用时间换取空间的反常类型题目,二分法用到了抽屉原理,
抽屉原理:桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。
我们定好left和right后,求出mid,然后统计nums中小于等于mid的数的个数,如果大于mid则说明重复值在mid及其左边,否则在其右边,代码如下:
int findDuplicate(vector<int>& nums) {
int n=nums.size();
int left=1,right=n-1;
while(left<right){
int mid=left+((right-left)>>1);
int count=0;
for(int i=0;i<n;++i){
if(nums[i]<=mid) ++count;
}
if(count>mid) right=mid;
else left=mid+1;
}
return left;
}
这里不能使用 left=mid 的策略,因为我们必须统计小于等于 mid 的值来和 mid 比较,而不是大于等于 mid 的值,也就是要始终保证目标值不在right的右侧。如果用 left=mid 方案,right=mid-1 ,这将引起错误。
复杂度
时间复杂度:O(NlgN)
空间复杂度:O(1)
4、先序/后序遍历构造二叉树
题目
分析
这里使用二分法进行构造,如下:
TreeNode* preordertobst(vector<int>& preorder,int left,int right){
if(left>right) return NULL;
TreeNode* root=new TreeNode(preorder[left]);
if(left==right) return root;
int l=left+1,r=right;
while(l<r){
int m=l+((r-l+1)>>1);
if(preorder[m]<preorder[left]) l=m;
else r=m-1;
}
if (preorder[l] > preorder[left]) --l;
root->left=preordertobst(preorder,left+1,l);
root->right=preordertobst(preorder,l+1,right);
return root;
}
这里要注意两个地方:
1)if(left==right) return root;
如果没有这一步,因为 root->left=preordertobst(preorder,left+1,l);
由于l=left+1,所以当 l>=r 时,这里会一直往后递归,直到到preorder的末尾,然后报错。
2)if (preorder[l] > preorder[left]) --l;
假如输入为 [10,12] ,由于这里 l==r ,所以会直接进入 root->left=preordertobst(preorder,left+1,l);
把12变成10的左子树,造成错误。
更正版本:
TreeNode* preordertobst(vector<int>& preorder,int left,int right){
if(left>right) return NULL;
int flag=preorder[left];
TreeNode* root=new TreeNode(flag);
int l=left,r=right;
while(l<r){
int mid=l+((r-l+1)>>1);
if(preorder[mid]>flag) r=mid-1;
else l=mid;
}
root->left=preordertobst(preorder,left+1,l);
root->right=preordertobst(preorder,l+1,right);
return root;
}
这里为便于说明,特意将以前的版本留在了上面,可以看到,在将 l=left 后,上述需要注意的两点问题都会消失。
后序遍历构造二叉树和前序差不多,把判断位置从left改到right,l=left,r=right-1 以及
root->left=preordertobst(preorder,left,l);
root->right=preordertobst(preorder,l+1,right-1);
注意:这里if (preorder[l] > preorder[right]) --l; 不能丢,假设输入为 [12,10] 分析可知。
复杂度
时间复杂度: O(lgN)
空间复杂度 :O(1)
5、寻找两个正序数组的中位数
题目
分析
参考:link
这里先秉持循序渐进的原则,先考虑下O(m+n) 的做法,即维护两个指针,以类似归并排序的方式,找出中间数。
这题比较cd的一点就是要分奇偶,奇数情况下好说,找到第 n/2 + 1 个数即可。而偶数情况下则需要同时找到第 n/2 和第 n/2+1个数,然后求平均值。这里的处理方式就是无论奇数还是偶数,都把第 n/2 和第 n/2+1个数找出来,然后进行讨论。代码如下:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int n1 = nums1.size(), n2 = nums2.size();
int n = n1 + n2;
int mid = (n >> 1);
int p1 = 0, p2 = 0;
double mid1 = 0, mid2 = 0;
while (mid) {
if (mid == 1) {
if (p1 < n1&&p2 < n2) mid1 = min(nums1[p1], nums2[p2]);
else if (p1 >= n1) mid1 = nums2[p2];
else if (p2 >= n2) mid1 = nums1[p1];
}
if (p1 < n1&&p2 < n2) {
if (nums1[p1] < nums2[p2]) ++p1;
else ++p2;
}
else if (p1 >= n1) ++p2;
else if (p2 >= n2) ++p1;
--mid;
}
if (p1 < n1&&p2 < n2) mid2 = min(nums1[p1], nums2[p2]);
else if (p1 >= n1) mid2 = nums2[p2];
else if (p2 >= n2) mid2 = nums1[p1];
if (n % 2) return mid2;
return (mid1 + mid2) / 2;
}
这里一个是要注意检查是否有数组已经用完了;
还有一个是 mid 的选取,这里注意mid表示的是要丢弃的数的数量,因为我们要找到第 n/2+1个数,所以要丢弃 n/2个数,同时在 mid=1 的时候也要做次记录,这是第 n/2 个数。还有一点就是这里检查的位置要放在while的最开始处,因为mid有可能就等于1,例如:
[]
[2,3]
6、寻找峰值
题目
分析
参考:link
这里要注意 “nums[-1] = nums[n] = -∞” 这个条件,我们从 0 开始思考,如果位置 1 的数小于位置 0 的数,那它就是峰值,否则它就会这样一直递增下去。 n-1 也是同理,它在遇到峰值前会一直递减下去。
这样下去最后两段会交汇于一点,这一点即为峰点,或者存在一个以上的峰点。
总之,所谓峰点就是左侧在递增,右侧在递减,最后交汇于一点。
这里我们可以任选一个为标准,比如以左侧为标准:
1、找到mid
2、如果 nums[mid-1 ]<nums[mid],说明峰值在mid右侧,取 left=mid
3、反之,峰值在mid左侧,取 right=mid-1
代码如下:
int findPeakElement(vector<int>& nums) {
int n=nums.size();
if(n==1) return 0;
if(nums[0]>nums[1]) return 0;
if(nums[n-1]>nums[n-2]) return n-1;
int left=0,right=n-1;
int ret=0;
while(left<right){
int mid=left+((right-left+1)>>1);
if(nums[mid]>nums[mid-1]&&nums[mid]>nums[mid+1]){
ret=mid;
break;
}
if(nums[mid]>nums[mid-1]) left=mid;
else right=mid-1;
}
return ret;
}
复杂度
时间复杂度:O(lgN)
空间复杂度:O(1)
7、搜索二维矩阵I&II
题目
分析
搜索二维矩阵
参考:link
这里注意到如果把矩阵一行一行取出再串起来可以得到一个单调递增的数组,既然遇到了单调,那么自然而然二分法没跑了……
如参考中所说的,这里要如果有一个数在一维坐标位置是loc,那么它在二维坐标就是[loc/col][loc%col],代码如下:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int row = matrix.size();
if (row == 0) return false;
int col = matrix[0].size();
if (col == 0) return false;
int left = 0, right = row * col - 1;
int x = 0, y = 0;
while (left < right) {
int mid = left + ((right - left + 1) >> 1);
x = mid / col;
y = mid % col;
if (matrix[x][y] == target) return true;
else if (matrix[x][y] > target) right = mid - 1;
else left = mid;
}
return matrix[left/col][left%col] == target;
}
这里注意还要检查要最后退出迭代时的值是否为目标数。
复杂度
时间复杂度:O(log(mn)) = O(log(m) + log(n))
空间复杂度:O(1)
搜索二维矩阵II
参考:link
这里一行一列的进行排除,不断地逼近结果值,这里的关键在于要以左下角为起点,然后向右上角逼近。
因为这样做时,
如果目标值比当前值小,则当前行全都大于目标值,直接上移一行。
如果目标值比当前值大,则当前列全部小于目标值,直接右移一列。
代码如下:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int row=matrix.size();
if(row==0) return false;
int col=matrix[0].size();
int x=row-1,y=0;
while(x>=0&&y<col){
if(matrix[x][y]==target)return true;
else if(target<matrix[x][y]) --x;
else ++y;
}
return false;
}
复杂度
时间复杂度:O(m+n)
空间复杂度:O(1)
8、数组中的逆序对
题目
分析
参考:link
这题并不是用的二分法,而是归并排序的分治思想,但这里权且将其放在二分法这部分中。
在分的时候啥也不用管,而合并时要充分运用归并时两个数组的有序性,如下图所示:
代码如下:
class Solution {
public:
int reversePairs(vector<int>& nums) {
int n=nums.size();
maxlen=0;
vector<int> helper=nums;
mergesort(nums,helper,0,n-1);
return maxlen;
}
void mergesort(vector<int>&a,vector<int>&b,int left,int right){
if(left>=right) return;
int mid=left+((right-left)>>1);
mergesort(a,b,left,mid);
mergesort(a,b,mid+1,right);
int i=left,j=mid+1,ptr=left;
while(i<=mid&&j<=right){
//注:如果出现b[i]==b[j]的情况,应该让i直接填到ptr中。
if(b[i]<=b[j]){
a[ptr]=b[i];
++i;
}
else{
a[ptr]=b[j];
++j;
maxlen+=(mid-i+1);
}
++ptr;
}
while(i<=mid){
a[ptr]=b[i];
++ptr;
++i;
}
while(j<=right){
a[ptr]=b[j];
++ptr;
++j;
}
for(int k=left;k<=right;++k) b[k]=a[k];
return;
}
private:
int maxlen;
};
这里要注意代码中注释的内容,当出现 b[i]==b[j] 时,应该让 b[i] 直接填到 a[ptr] 中,因为此时如果执行else中的代码,那么这里就会多加一个数,也就是 b[i] 。