33. 搜索旋转排序数组
引言
by 三叶题解
- 在有序数组中搜索,可以考虑使用 【二分】求解。
- 「二分」的本质是两段性,并非单调性。只要一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。
本题由于发生了“旋转”,整体上并不是纯粹的“有序数组”,所以首先需要找到“旋转点”的位置,而由于“旋转点”分割的左、右子数组 则均为“有序” --> 可以二分。
查找“旋转点”的位置又可以有两种方法,即“线性遍历”、或“利用两段性”使用二分来找“旋转点”,具体的两种解法如下所示。
二分(线性遍历旋转点)
思路
- 线性扫描
n
u
m
s
nums
nums 找到“旋转点”位置
index
; - 第一次二分:在 [ 0 , i n d e x + 1 ) [0, index + 1) [0,index+1) 二分查找 t a r g e t target target 是否存在;
- 第二次二分:在 [ i n d e x + 1 , n ) [index + 1, n) [index+1,n) 二分查找 t a r g e t target target 是否存在;
class Solution {
public int search(int[] nums, int target) {
int n = nums.length;
// 找旋转点
int index = 0;
for ( ; index + 1 < n && nums[index] < nums[index + 1]; index++) {}
System.out.println("index = " + index);
// 1、在[0, index + 1)二分
int res = binarySearch(nums, 0, index + 1, target);
if (res != -1) {
return res;
}
// 2、在[index + 1, n)二分
res = binarySearch(nums, index + 1, n, target);
return res;
}
// 在nums下标为[left, right)中搜索target
public int binarySearch(int[] nums, int left, int right, int target) {
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid; // 左闭右开
} else {
return mid;
}
}
return -1; // 不存在
}
public static void main(String[] args) {
int[] nums = {1};
int target = 1;
System.out.println(new Solution().search(nums, target));
}
}
- 时间复杂度:最坏 O ( n ) O(n) O(n)(“旋转点”在 n u m s nums nums末尾,需要线性扫描整个 n u m s nums nums),最好 O ( l o g n ) O(logn) O(logn)(“旋转点”在 n u m s nums nums开头)
- 空间复杂度: O ( 1 ) O(1) O(1)
二分(二分找旋转点)
此种解法和 剑指Offer Day4 中记录的 剑指 Offer 53 - I. 在排序数组中查找数字 I 很类似,但又有所不同。
by 三叶题解
-
「二分」的本质是两段性,并非单调性。只要一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。
-
经过旋转的数组,显然前半段满足 > = n u m s [ 0 ] >= nums[0] >=nums[0],而后半段不满足 > = n u m s [ 0 ] >= nums[0] >=nums[0]。我们可以以此作为依据,通过「二分」找到旋转点。
class Solution {
public int search(int[] nums, int target) {
int n = nums.length;
// 第一次二分:找旋转点
int left = 0;
int right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= nums[0]) { // 大于等于nums[0],说明旋转点还在mid右边
left = mid + 1;
} else {
right = mid - 1; // 左闭右闭
}
}
int index = right; // 左边数组的最右边界,即[0, index]为左数组
System.out.println("index = " + index);
// 第二次二分:
if (target >= nums[0]) { // target“可能”在左数组中(必不可能在右数组)
left = 0;
right = index; // 左闭右闭
} else {
left = index + 1;
right = n - 1; // 左闭右闭
}
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1; // 左闭右闭
} else {
return mid;
}
}
return -1; // 不存在
}
}
- 时间复杂度: O ( l o g n ) O(logn) O(logn)
- 空间复杂度:
O
(
1
)
O(1)
O(1)
81. 搜索旋转排序数组 II
引言
直接套用 33. 搜索旋转排序数组 二分找“旋转点”时,则 gg。
原因在于:本题中 n u m s nums nums可能存在重复元素,若“旋转”后将重复元素分在了左、右两个子数组中时,会造成 n u m s nums nums 丢失“两段性”。
如下将 n u m s = [ 0 , 1 , 1 , 1 , 1 ] nums=[0, 1, 1, 1, 1] nums=[0,1,1,1,1] “旋转后”,得到 [ 1 , 0 , 1 , 1 , 1 ] [1, 0, 1, 1,1] [1,0,1,1,1]。其中,“旋转点”为 i n d e x = 0 index = 0 index=0,此时 左子数组(即, [ 1 ] [1] [1]) 中元素均 > = n u m s [ 0 ] >=nums[0] >=nums[0]成立,但是 右子数组(即, [ 0 , 1 , 1 , 1 ] [0, 1, 1, 1] [0,1,1,1])中“所有””元素均 > = n u m s [ 0 ] >=nums[0] >=nums[0] 并非恒成立。即,此时“丢失两段性”,需要经过维护“两段性”后,才能使用 二分。
- by 三叶题解
二分(维护“两段性”)
如上分析,需要首先维护“两段性”,使得 左子数组中所有元素 > = n u m s [ 0 ] >= nums[0] >=nums[0]、右子数组中所有元素 < n u m s [ 0 ] < nums[0] <nums[0]时,才可以使用二分。
class Solution {
public boolean search(int[] nums, int target) {
// 找“旋转点”
int n = nums.length;
int left = 0;
int right = n - 1;
while (left < right && nums[0] == nums[right]) { // 恢复“二段性”
right--;
}
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= nums[0]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
int index = right; // “旋转点”,即[0, index]为左子数组
System.out.println("index = " + index);
// 第二次二分
if (target >= nums[0]) { // target只“可能”在左子数组中
left = 0;
right = index; // 左闭右闭
} else { // target只“可能”在右子数组中
left = index + 1;
right = n - 1; // 左闭右闭
}
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1; // 左闭右闭
} else {
return true;
}
}
return false;
}
}
- 时间复杂度:最坏 O ( n ) O(n) O(n)(维护“两段性”需要线性扫描整个 n u m s nums nums),最好 O ( l o g n ) O(logn) O(logn)(不用维护“两段性”)
- 空间复杂度:
O
(
1
)
O(1)
O(1)