概念引入
旋转排序数组 就是一个递增数组(暂且不论严格/非严格)在某一个点处进行了旋转。
为了避免“旋转”这个词的歧义,下面用一个例子直观展示:
原递增数组:[1, 2, 3, 4, 5, 6, 7]
旋转后数组:[4, 5, 6, 7, 1, 2, 3]
性质分析
第一点:旋转多次等价于旋转一次
不要被“旋转”这两个字误导,透过现象看本质,所谓的 旋转 其实就是 循环移动 —— 你看成循环左移/循环右移都可以,本质还是 模(mod)。
在纸上写写画画,比凭空想象有用。
第二点:变换后的数组仍保留着部分有序性
由于多次变换等价于一次变换,所以不管数组“旋转”了多少次,最后还是等价于“旋转”了一次——变为了两段递增序列。
这种尚未完全消失的递增性质,也是下面我们依旧可以使用二分的前提。
图解二分
这类题目的关键技巧是:以右端点 nums[right] 为基准,作为二分减治的依据。
假设原数组严格递增(不存在重复),根据下面两种情况进行二分减治:
假设原数组非严格递增(存在重复数字),那么我们无从判断是下面的哪一种情况,解决的方案是从右侧开始收缩重复元素(反正存在重复,起码nums[mid]==nums[right],所以可以放心收缩掉):
题目1. 寻找旋转数组中的最小值
假设原数组严格递增(不存在重复)
class Solution {
public int findMin(int[] nums) {
int left = 0;
int right = nums.length - 1;
int mid;
while (left < right) {
mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) {
left = mid + 1;
} else {
right = mid;
}
}
return nums[left];
}
}
假设原数组非严格递增(存在重复数字)
class Solution {
public int findMin(int[] nums) {
int left = 0;
int right = nums.length - 1;
int mid;
while (left < right) {
mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) {
left = mid + 1;
} else if (nums[mid] < nums[right]) {
right = mid;
} else {
right -= 1;
}
}
return nums[left];
}
}
题目1. 寻找旋转数组中的目标值
假设原数组严格递增(不存在重复)
class Solution {
class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int mid;
while (left <= right) {
mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
if (nums[mid] < nums[right]) {
// 右侧保证有序
if (target <= nums[right] && target > nums[mid]) {
left = mid + 1;
} else {
right = mid - 1;
}
} else {
// 左侧保证有序
if (target >= nums[left] && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
}
return -1;
}
}
假设原数组非严格递增(存在重复数字)
class Solution {
public boolean search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int mid;
while (left <= right) {
mid = left + (right - left) / 2;
if (nums[mid] == target) {
return true;
}
if (nums[mid] < nums[right]) {
// 右侧必定有序
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
} else if (nums[mid] > nums[right]) {
// 左侧必定有序
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else if (nums[mid] == nums[right]) {
// 不能确定
while (left <= right && right > 1 && nums[right - 1] == nums[right]) {
right--;
}
right--;
}
}
return false;
}
}
总结分析
1)如果不存在重复元素,那么就是O(logn)的时间复杂度;如果存在重复元素,那么就是O(logn)的平均时间复杂度、O(n)的最坏时间复杂度——所有元素均相同,右侧一个个收缩的极端情况。
2)题目1找最小元素,题目2找目标元素,恰好就是二分法的两大模板(收缩模板 & 命中模板)。如果对二分法的模板仍存在疑惑,可以戳这里,从此彻底不再惧怕二分。
3)理解减治,比背诵二分更重要。
E N D END END