这两天看这个看晕乎了...痛定思痛,必须学会!
讲解
左闭右闭:
left = 0, right = nums.size() - 1
找target
进入while循环,while(),闭区间[1, 1]合法吗?合法,那么
while(left <= right)
mid = (left + right) / 2
if(mid > target) right = mid - 1;//不用=,因为mid一定不是
else if(mid < target) left = mid + 1;
else return mid
跳出while循环之后,return -1
左闭右开:
left = 0,right = nums.size()
[1,1)合法吗?不合法!所以while(left < right),不可以等于
mid = (left + right) / 2
if(mid > target) right = mid
else if(mid < target) left = mid + 1;
else return mid
跳出while,return -1
左开右开:
left = 0, right = nums.size()
while(left + 1 != right)
mid = (left + right) / 2;
if(mid > target) right = mid
else left = mid;
跳出while,return right
()是最容易的写法
寻找旋转排序数组中的最小值
无重复元素,所有元素都不一样
旋转数组这一系列题有一点非常怪,tmd,那就是12345不可能旋转成54321,所以开区间那边都是nums.size() - 1
二分法需要保证的不是连续性,而是二段性质,简单点来说,就是这序列的数组有没有一个明显的界限分成两部分,这个题显然有,那么直接套模板是可以的,只是在修改left和right的时候需要注意
咱要找的是最小值,最小值在哪一段?右边那一段,所以我们选择nums.back()作为比较点,接下来,以开区间作为咱的写法,left = -1, right = nums.size() - 1,这里right为什么这么写上面已经提过了
if(nums[mid] < nums.back()),说明咱们要找的最小值的点还在更左边,那么right = mid
else left = mid;
最后仍然return nums[right]
class Solution {
public:
int findMin(vector<int>& nums) {
int left = -1, right = nums.size() - 1;
while(left + 1 != right){
int mid = (left + right) / 2;
if(nums[mid] < nums.back()) right = mid;
else left = mid;
}
return nums[right];
}
};
寻找旋转排序数组中的最小值II
有重复元素~!
普通的二分法也只是要求数组有序而没有说必须无重复,但是在旋转数组中,咱们如果有重复数组的话会麻烦很多,主要体现在二段性的缺失。
举一个缺失二段线的例子:
由于在旋转点有元素重复,所以左边大于等于2,邮编小于等于2,都有等于2的元素,导致没办法分成特点鲜明的两端,那么怎么做呢?把二段性补回来即可
怎么补?也不简单,首先这个题很变态,12345是有效用例,54321就是无效的
所以假设哈,假设
开头和结尾出现了重复数,比如[5,6,7,1,2,3,4,5,5],第一段大于等于5,第二段小于等于5,这种情况right–和left++,将5去除,尚能解决,由于我说过了12345在这里是有效用例,54321就不是,所以我们选择right–
class Solution {
public:
int findMin(vector<int>& nums) {
// 恢复二段性
int n = nums.size();
int left = -1, right = n - 1;
while(left < right && nums[right] == nums[0]){
right--;
}
int flag = right;
// 找到旋转点
while (left + 1 < right){
int mid = (left + right) / 2;
if(nums[mid] < nums[flag]){
right = mid;
}
else{
left = mid;
}
}
return right == -1 ? nums[0] : nums[right];
}
};
以上right–可以解决头尾重复,但无法解决12334这种情况
为什么呢?
首先这种情况不在头尾重复缺失二段性的例子里面,当与back比较的时候,如果相等,按照上面的代码我们会使left = mid,但是最小值显然在左边,我们要改的是right才对
class Solution {
public:
int findMin(vector<int>& nums) {
// 恢复二段性
int n = nums.size();
int left = -1, right = n - 1;
while(left < right && nums[right] == nums[0]){
right--;
}
int flag = right;
// 找到旋转点
while (left + 1 < right){
int mid = (left + right) / 2;
if(nums[mid] <= nums[flag]){
right = mid;
}
else{
left = mid;
}
}
return right == -1 ? nums[0] : nums[right];
}
};
小总结
以上两道题我小总结一下,有无重复元素影响的是=加不加,其实第一题也可以加,在无重复元素的时候,开区间加不加其实没所谓,但是当有重复元素的时候,考虑这个很重要
我在刚开始学习二分法的时候,背诵的模板的不加等号的if,改的是left,其实我如果照着这个模板写这两道题,就不会错,但是我鬼使神差把不加等号那边改right了,导致一直不过
那么可以总结一个有无重复元素通用的模板
针对第一题,第二题是一样的:
class Solution {
public:
int findMin(vector<int>& nums) {
int left = -1, right = nums.size() - 1;
while(left + 1 != right){
int mid = (left + right) / 2;
if(nums[mid] > nums.back()) left = mid;
else right = mid;
}
return nums[right];
}
};
搜索选择排序数组
无重复元素,找target下标
先找最小值,然后和最小值比较,往哪边找
开区间改left是好习惯!
class Solution {
public:
int findMin(vector<int>& nums){
int left = -1, right = nums.size() - 1;
while(left + 1 != right){
int mid = (right + left) / 2;
if(nums[mid] > nums.back()) left = mid;
else right = mid;
}
return right;
}
int lower_bound(vector<int>& nums, int left, int right, int target){
while(left + 1 != right){
int mid = (left + right) / 2;
if(nums[mid] < target) left = mid;
else right = mid;
}
if(right == nums.size()) return -1;
return nums[right] == target ? right : -1;
}
int search(vector<int>& nums, int target) {
int index = findMin(nums);
if(target > nums.back()) return lower_bound(nums, -1, index, target);
else return lower_bound(nums, index - 1, nums.size(), target);
}
};
搜索旋转排序数组II
有重复元素,找target下标
哎呀,这题就好难了
如果按照之前的做法,会败在用例[2,2,2,3,2,2,2]
为什么呢?
因为后面的2在一开始就被right–掉了,剩下前面一节2和3,这时候返回的最小值一定是第一个2,即index = 0,要找的是3,-1和index之间哪里有3呢?
理想情况,我们要找的最小值应该是[2,2,2,3,2,2,2]中间标黄的2
md这就是这一系列题很恶心的地方,写的烦死了
之后我想到一个完美的解决方案过这个用例,那就是,两边都return一下,只要有true就是true,都是false才是false,完美!
更改的关键代码:
if(!lower_bound(nums, -1, index, target)){
return lower_bound(nums, index - 1, flag, target);
}
else return true;
新的问题来了,right–后相当于后面一节相似的不要了,假如数组是22222的话,right会一直减,直到为-1,那么flag也会是-1,之后right可以在return部分纠正为0,但flag还会是-1,所以这里的flag需要纠正:
flag = right;
if(right == -1) flag = 0;
总代码:
class Solution {
public:
int flag;
int findMin(vector<int>& nums){
int left = -1, right = nums.size() - 1;
while(left < right && nums[right] == nums[0]) right--;
flag = right;
if(right == -1) flag = 0;
while(left + 1 < right){
int mid = (right + left) / 2;
if(nums[mid] > nums[right]) left = mid;
else right = mid;
}
return right == -1 ? 0 : right;
}
bool lower_bound(vector<int>& nums, int left, int right, int target){
while(left + 1 < right){
int mid = (left + right) / 2;
if(nums[mid] < target) left = mid;
else right = mid;
}
if(right == nums.size()) return false;
return nums[right] == target ? true : false;
}
bool search(vector<int>& nums, int target) {
int index = findMin(nums);
if(!lower_bound(nums, -1, index, target)){
return lower_bound(nums, index - 1, flag, target);
}else{
return true;
}
}
};
小总结
说实话,我感觉自己在面向用例编程,最后的代码并不优美,但也是我自己一点点想出来的,也更加熟悉了二分法,每道题都比前面的题目多一点点特殊情况,很头疼,我在想,这种题目考我的话,思考还不如背诵,思考我还需要用例,但是换个环境哪有用例?找别人优美的代码学一学,背一背,可能比自己孤军奋战折腾出来更好,最后附上自己的提交记录,因为真的不容易。。