题目:
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。
例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为
数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,
并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须尽可能减少整个过程的操作步骤。
-----------------------------
示例 1:
输入:nums = [1,3,5]
输出:1
输入:nums = [2,2,2,0,1]
输出:0
提示:
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转
进阶:这道题与 寻找旋转排序数组中的最小值 类似,但 nums 可能包含重复元素。
允许重复会影响算法的时间复杂度吗?会如何影响,为什么?
---------------------
思路:
寻找旋转数组的最小元素即为寻找 右排序数组 的首个元素 nums[x] ,称 x 为 旋转点 。
排序数组的查找问题首先考虑使用 二分法 解决,
其可将 遍历法 的 线性级别 时间复杂度降低至 对数级别 。
--------------
算法流程:
初始化: 声明 i, j 双指针分别指向 nums 数组左右两端;
循环二分: 设 m = (i + j) / 2为每次二分的中点( "/" 代表向下取整除法,因此恒有i≤m<j),
可分为以下三种情况:
当 nums[m] > nums[j]时: m 一定在 左排序数组 中,即旋转点 x 一定在 [m + 1, j] 闭区间内,因此执行 i = m + 1
当 nums[m] < nums[j] 时: m 一定在 右排序数组 中,即旋转点 x 一定在[i, m]闭区间内,
因此执行 j = m
当 nums[m] = nums[j]时: 无法判断 m 在哪个排序数组中,
即无法判断旋转点 x 在 [i, m]还是 [m + 1, j]区间中。
解决方案: 执行 j = j - 1缩小判断范围,分析见下文。
返回值: 当 i = j时跳出二分循环,并返回 旋转点的值 nums[i]即可。
---------------
补充思考: 为什么本题二分法不用 nums[m]和 nums[i]作比较?
二分目的是判断 m 在哪个排序数组中,从而缩小区间。而在 nums[m] > nums[i]情况下,无法判断 m 在哪个排序数组中。本质上是由于 j 初始值肯定在右排序数组中; i 初始值无法确定在哪个排序数组中。
-------
复杂度分析:
时间复杂度 O(log2N) : 在特例情况下(例如 [1, 1, 1, 1]),会退化到 O(N)。
空间复杂度 O(1) : i , j , m 变量使用常数大小的额外空间。
class Solution {
public int minArray(int[] numbers) {
int left = 0, right = numbers.length - 1;
while (left < right) {
int mid = left + ((right - left) >> 1);
if (numbers[mid] > numbers[right]) {
left = mid + 1;
}else if (numbers[mid] < numbers[right]){
right = mid;
}else right--;
}
return numbers[right];
}
}
LC