题目1:
整数数组 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,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题目2:
已知存在一个按非降序排列的整数数组 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:
输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
解题思路:
题目1:
方法1:
一次遍历
方法2:
区间二分
有两种具体的实现:
一种是按照分好的两个区间直接开始遍历;
第二种是第一次二分找到分割点,第二次二分根据该分割点进行遍历。
题目2:
方法:
同样是两种。
第一种是因为存在相同元素,当无法判断前后区间哪个有序时,需要继续偏移寻找有序的区间;
第二种是恢复二段性后,先找旋转点,后找目标值
技术总结:
最近被二分虐的死去活来,通过广泛阅读各种大佬的总结和模板,自己整理一下心得:
- 理解二分的本质。
这也是我最近学习宫水三叶大佬的文章悟出的。
二分的本质不是单调,而是两段性。 只要某一段满足某个性质,而另一段不满足,就可以使用二分来求解。
单调只是一个性质,我们从有序数组中找某个数只是二分的应用。
- 二分难在细节。
不同的问题写法都有区别,我们最重要的是根据题目能够写出模板,而不是背出模板。
在数组中,我们一般会遇到两种情况,这两种情况本质上是没有区别的:
- 找到某个固定元素
- 找到满足条件的 第一个 / 最后一个元素
- 二分模板:
网上有两种不同的模板,一种是闭区间,另一种是左开右闭区间。 其中,左开右闭区间的写法更加灵活,更适合情况较为复杂的题目。
「二分」模板其实有两套,主要是根据 check(mid) 函数为 true 时,需要调整的是 l 指针还是 r 指针来判断。
• 当 check(mid) == true 调整的是 l 时:计算 mid 的方式应该为 mid = l + r + 1 >> 1:
/* check(mid)就是根据题意给出表达式:
例如当求x ≥ target 的第一个元素时,使用模板2,check(mid)为 nums[i] >= target
*/
//模板1:求最后一个满足该条件的值
//没有越界风险是考虑int
long l = 0, r = n - 1;
while (l < r) {
//向下取整, +1 操作主要是为了避免发生「死循环」
long mid = l + r + 1 >> 1;
//推荐写法 int mid = l +( r - l) / 2 + 1;
if (check(mid)) {
//偏移l, 求最后一个满足该条件的值
l = mid;
} else {
r = mid - 1;
}
}
• 当 check(mid) == true 调整的是 r 时:计算 mid 的方式应该为 mid = l + r >> 1:
//模板2:求第一个满足该条件的值
long l = 0, r = n - 1;
//左开右闭区间
while (l < r) {
long mid = l + r >> 1;
//推荐写法 int mid = l +( r - l) / 2;
if (check(mid)) {
//偏移r, 求第一个满足该条件的值
r = mid; //
} else {
l = mid + 1;
}
}
代码
//题目1
//直接按照两个不同的区间进行查找
class Solution {
public int search(int[] nums, int target) {
int len= nums.length;
//special
if (len == 0 || nums == null) {
return -1;
}
//区间二分
int l = 0;
int r= len - 1;
int mid = 0;
while (l <= r) {
mid = ( r - l) / 2 + l;
//按照区间二分
if (nums[mid] == target) {
return mid;
}
//前半区间
if (nums[0] <= nums[mid]) {
//前半段
if (target >= nums[0] && target <= nums[mid]) {
r = mid - 1;
} else {
l = mid + 1;
}
} else {
//后半段
if (target <= nums[len - 1] && target >= nums[mid]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
}
return -1;
}
}
//第一次二分 找到分割点,第二次二分进行遍历
class Solution {
public int search(int[] nums, int target) {
int n = nums.length;
//special
if (n == 0 || nums == null) {
return -1;
}
if (n == 1 && nums[0] == target) {
return 0;
}
int l = 0;
int r = n - 1;
//normal
//第一次二分 找到idx
//找到分割点 因为分割点在大小交界的位置,所以如果大于首位置数值,需要继续向后偏移;小于则向前偏移
while (l < r) {
int mid = l + r + 1 >> 1; //偏移一个位置开始计算
if (nums[mid] >= nums[0]) {
l = mid;
} else {
r = mid - 1;
}
}
//第二次二分 找到target
int idx = 0;
if (target >= nums[0]) {
idx = find (nums, 0, r, target);
} else {
idx = find (nums, l + 1, n - 1, target);
}
return idx;
}
public int find(int[] nums, int l, int r, int t) {
int mid = 0;
while (l < r) {
mid = l + r >> 1;
if (nums[mid] >= t) {
r = mid;
} else {
l = mid + 1;
}
}
return nums[r] == t? r: -1;
}
}
//题目二
//方法1:因为存在相同元素,当无法判断前后区间哪个有序时,需要继续偏移寻找有序的区间
class Solution {
public boolean search(int[] nums, int target) {
int len= nums.length;
//special
if (len == 0 || nums == null) {
return false;
}
//区间二分
int l = 0;
int r= len - 1;
int mid = 0;
while (l <= r) {
mid = ( r - l) / 2 + l;
//按照区间二分
if (nums[mid] == target) {
return true;
}
//只偏移前面的
if (nums[l] == nums[mid]) {
l++;
continue;
}
//前半区间
if (nums[l] < nums[mid]) {
//前半段
if (target >= nums[l] && target <= nums[mid]) {
r = mid - 1;
} else {
l = mid + 1;
}
} else {
//后半段
if (target <= nums[len - 1] && target >= nums[mid]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
}
return false;
}
}
//方法2
//恢复二段性后,先找旋转点,后找目标值
class Solution {
public boolean search(int[] nums, int t) {
int n = nums.length;
int l = 0, r = n - 1;
// 恢复二段性
while (l < r && nums[0] == nums[r]) r--;
// 第一次二分,找旋转点
while (l < r) {
int mid = l + r + 1 >> 1;
if (nums[mid] >= nums[0]) {
l = mid;
} else {
r = mid - 1;
}
}
int idx = n;
if (nums[r] >= nums[0] && r + 1 < n) idx = r + 1;
// 第二次二分,找目标值
int ans = find(nums, 0, idx - 1, t);
if (ans != -1) return true;
ans = find(nums, idx, n - 1, t);
return ans != -1;
}
int find(int[] nums, int l, int r, int t) {
while (l < r) {
int mid = l + r >> 1;
if (nums[mid] >= t) {
r = mid;
} else {
l = mid + 1;
}
}
return nums[r] == t ? r : -1;
}
}