2020年8月3日 周一 天气阴 【不悲叹过去,不荒废现在,不惧怕未来】
本文目录
1. LeetCode153. 寻找旋转排序数组中的最小值
1.1 题目简介
LeetCode 153. 寻找旋转排序数组中的最小值
首先说一下题目中的“旋转”,其实就是循环右移,“旋转”将数组分为两部分,并且这两部分都是升序。
1.2 二分法变形(重点考虑边界问题)
虽然数组被“旋转”了,但是仍然可以用二分法进行求解,只不过需要稍微进行修改。先复习一下传统的二分搜索:
int BinarySearch(vector<int>& nums, int target) {
int l = 0, r = nums.size()-1;
while (l <= r) {
int m = (r + l) / 2;
if (nums[m] == target)
return m;
else if(nums[m] < target)
l = m + 1;
else
r = m - 1;
}
return -1;
}
1.2.1 nums[m] < nums[0] 和 nums[m] < nums[len-1] 通过
在这里,我们需要找到数组中最小的元素,首先判断数组是否被“旋转”,如果没有,直接返回数组的首元素即为最小元素。
如前所述,如果数组被“旋转”了,数组会被分为左右两部分,并且这两部分都是升序,而最小元素为数组右部分的第一个元素。我们将nums[m]和数组的首元素nums[0]进行比较,如果nums[m]比较小,那可以肯定m所在的位置为数组右部分,并且不能确定nums[m]是否为最小元素,因此令r = m。反之如果nums[m]比较大,那可以肯定m所在的位置为数组左部分,并且能确定nums[m]不是最小元素,因此令l = m + 1。
这样循环下去,最后会令l和r均指向最小元素所在的位置,因此返回nums[l]和nums[r]都可以。
代码如下:
class Solution {
public:
int findMin(vector<int>& nums) {
int len = nums.size();
int l = 0, r = len-1;
if(nums[l]<nums[r]) return nums[l];
while(l<r){
int m = (l+r)/2;
if(nums[m]<nums[0])
r = m;
else
l = m + 1;
}
return nums[l];
}
};
另外,如果把nums[m] < nums[0]改成nums[m] < nums[len-1],效果是一样的,因为无论是将nums[m]和首元素还是尾元素进行比较,都能看出nums[m]是属于数组左半部分还是右半部分,从而决定是移动l还是r。
class Solution {
public:
int findMin(vector<int>& nums) {
int len = nums.size();
int l = 0, r = len-1;
if(nums[l]<nums[r]) return nums[l];
while(l<r){
int m = (l+r)/2;
// nums[m]<nums[0]改成nums[m]<nums[len-1],效果一样
if(nums[m]<nums[len-1])
r = m;
else
l = m + 1;
}
return nums[l];
}
};
1.2.2 nums[m] < nums[r] 通过,nums[m] < nums[l] 不通过
那么问题来了,我能不能就让nums[m]和nums[r]或nums[l]进行比较呢?如下所示,将代码中改成nums[m] < nums[r],测试了一下是能通过的。
class Solution {
public:
int findMin(vector<int>& nums) {
int len = nums.size();
int l = 0, r = len-1;
if(nums[l]<nums[r]) return nums[l];
while(l<r){
int m = (l+r)/2;
if(nums[m]<nums[r])
r = m;
else
l = m + 1;
}
return nums[l];
}
};
而将nums[m] < nums[r]改成nums[m] < nums[l]后,程序在提交的时候就报错了:
分析问题
这是因为,代码里无论是nums[m] < nums[r]还是nums[m] < nums[l],都是为了判断nums[m]是否在数组的右部分,而一旦满足表达式,nums[m]必为数组的右部分,因此r = m决定了右边界r的位置永远在数组的右部分,不会越界到左部分。但是左边界l就不一定那么本分了,如果nums[m]是左部分最后一个元素,那么l = m + 1这行代码执行完之后,l就会指向右部分的第一个元素。
以上面输入的[3,4,5,1,2]为例:
[3,4,5,1,2]
第一次循环nums[m] = 5,nums[l] = 3,nums[m] > nums[r],所以l = m + 1 = 3,r仍然是4,即:l指向1,r指向2。第二次循环,m = (l + r) / 2 = 3,即:nums[m] == nums[l],所以执行l = m + 1,得到l = r = 4。退出循环,输出nums[4]即为2。
所以我们看到,一旦l出现在数组右部分之后,nums[m] < nums[l]这个判断条件就有可能误判nums[m]的位置,使结果出错。那么该如何解决这个问题呢?
*最终优化代码
一个很好的解决办法是:我们在循环条件中一直判断l到r是否为升序,如果是升序,说明l第一次出现在了数组右部分,这时候返回nums[l]即为数组最小元素。这样做也把刚开始判断数组是否为升序融合了进去,可谓一箭双雕。不过需要注意的是,最后返回的只能是nums[l]。
代码如下:
class Solution {
public:
int findMin(vector<int>& nums) {
int len = nums.size();
int l = 0, r = len-1;
// 每次循环判断nums[l]和nums[r]的大小,如果nums[l]<nums[r],则l到r为升序,直接返回nums[l]即为最小值
while(l<r && nums[l] > nums[r]){
int m = (l+r)/2;
if(nums[m]<nums[l])
r = m;
else
l = m + 1;
}l
return nums[l];
}
};
值得一提的是,这样做之后,无论把nums[m] < nums[l]中的nums[l]改成nums[r]或nums[0]或nums[len-1],都是可以的。
2. LeetCode154. 寻找旋转排序数组中的最小值 II
2.1 题目简介
LeetCode 154. 寻找旋转排序数组中的最小值 II
2.2 二分法变形
2.2.1 nums[m]==nums[r] ?
这回,数组里元素允许重复了。不过解决办法也很简单,我们只需要在上题题解的基础上,加上一个判断条件:如果nums[m]==nums[r],则- -r,也就是把右边界往里面收缩一个位置,这样满足l = r的位置就是最终结果。代码如下:
class Solution {
public:
int findMin(vector<int>& nums) {
int len = nums.size();
int l = 0, r = len-1;
if(nums[l]<nums[r]) return nums[l];
while(l<r){
int m = (l+r)/2;
if(nums[m]<nums[r])
r = m;
else if(nums[m]>nums[r])
l = m + 1;
else
--r;
}
return nums[l];
}
};
2.2.2 nums[m]==nums[l] ?
当然啦,正如我们在1.2.2中提到的,如果想让nums[m]和nums[l]进行比较,只需要把判断数组是否为升序这一步放在循环条件中就行了。相应的,- -r也变成了++l。
代码如下:
class Solution {
public:
int findMin(vector<int>& nums) {
int len = nums.size();
int l = 0, r = len-1;
while(l<r && nums[l]>=nums[r]){
int m = (l+r)/2;
if(nums[m]<nums[l])
r = m;
else if(nums[m]>nums[l])
l = m + 1;
else
++l;
}
return nums[l];
}
};
3. LeetCode33. 搜索旋转排序数组
3.1 题目简介
LeetCode33. 搜索旋转排序数组
这道题和”LeetCode153. 寻找旋转排序数组中的最小值“很像,只不过这里是搜索一个确定的值是否存在。
3.2 二分法变形
思路
- nums[m]把数组分成两部分,其中一部分一定是升序;
- 先将nums[m]和nums[l]或nums[r]进行比较,判断哪一部分是升序;
- 再根据target是否属于升序部分移动边界r或l。
代码如下:
class Solution {
public:
int search(vector<int>& nums, int target) {
int l=0, r=nums.size()-1;
while(l<=r){
int m = l+(r-l)/2;
if(nums[m]==target) return m;
//nums[r]改成nums[l]效果一样,都能判断哪一部分是升序的
if(nums[m]>=nums[l]){
// 当搞不清楚边界情况该不该合并时,就分开写
if(target==nums[l]) return l;
else if(target>nums[l] && target<nums[m]) r=m-1;
else l=m+1;
}
else{
// 当搞不清楚边界情况该不该合并时,就分开写
if(target==nums[r]) return r;
if(target>nums[m] && target<nums[r]) l=m+1;
else r=m-1;
}
}
return -1;
}
};
4. LeetCode81. 搜索旋转排序数组 II
4.1 题目简介
4.2 二分法变形
思路
大致思路和上一题一样,只不过这次数组中可能有重复元素,处理方法和”2. LeetCode154. 寻找旋转排序数组中的最小值 II“一样:如果中间值nums[m]和边界值nums[r]或nums[l]重复了,就把相应边界往里面收缩一个位置。
代码如下:
class Solution {
public:
bool search(vector<int>& nums, int target) {
int len = nums.size();
int l = 0, r = len-1;
while(l<=r){
int m = (l+r)/2;
if (nums[m] == target)
return true;
// nums[r]也可以改成nums[l],那么--r就变成了++l,下面也是nums[m]和nums[l]进行比较了
if(nums[m]==nums[r])
--r;
else if(nums[m]<nums[r]){
if(target>nums[m]&&target<=nums[r]) l = m+1;
else r = m-1;
}
else {
if(target>=nums[l]&&target<nums[m]) r = m-1;
else l = m+1;
}
}
return false;
}
};
注意:nums[m]==nums[r]也可以改成nums[m]==nums[l],那么- -r就相应变成了++l,后面也必须是nums[m]和nums[l]进行比较了。
总结
感觉这类二分搜索法的变形问题非常灵活,细节拉满,一不小心就会出错。我的建议是要多刷一些这类题目,积累经验,二分法变形的题见多了,有时候脑子都不用怎么思考,手自然而然地就把代码写出来了。
参考文献
https://leetcode-cn.com/problems/search-in-rotated-sorted-array/solution/duo-si-lu-wan-quan-gong-lue-bi-xu-miao-dong-by-swe/