「剑指Offer 11.旋转数组的最小数字」
Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
题目描述(level 简单)
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。给你一个可能存在 重复 元素值的数组 numbers
,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如,数组 [3,4,5,1,2]
为 [1,2,3,4,5]
的一次旋转,该数组的最小值为1
。
示例
- 示例1
输入:[3,4,5,1,2]
输出:1
- 示例2
输入:[2,2,2,0,1]
输出:0
思路分析
根据题意,就是将排序递增数组从开始的一段数字移动到并添加到数组尾部,排序且递增数组首先会想到使用二分法来解决,进一步转化,题目就是要求出数组的最小值并且时间与空间复杂度尽可能低。直接将数组排序然后返回nums[0]
。顺利解决,不过这样写的风险怕是要回家等通知了🤪。二分法,不断缩小范围,目标就是为了找到最小值所在的区间。分别设i,j
对应下标0, nums.length - 1
。“中间值”为nums[mid]
,以为示例数组[3,4,5,1,2]
,考虑一般通用的情况,首次计算时i = 0
,如果将nums[mid]
的值与nums[i]
作比较(nums[mid]>nums[0]
),此时无法判断最小值存在于哪个区间内([i,mid]、[mid,j]
)。而旋转后的新数组可以看成两个增序数组的拼接(一般情况下,不考虑特例)。那么最小值应该是在中部
附近,而j
本身就存在于右侧区间内,与nums[j]
比较是合理的。主要是处理逼近最小值时的区间边界处理,防止“错过”最小值。
- 定义
i,j,mid
其中mid = (i + j) / 2
将数组分成左右两个区间。 - 当
nums[mid]<nums[j]
时那么最小值在左区间此时移动区间指向j = mid
。 - 当
nums[mid]>nums[j]
时那么最小值在右区间,由于已经明确nums[mid]>nums[j]
则可以直接从下一个数开始遍历i = mid + 1
。 - 当
nums[mid]=nums[j]
时,也是关键的点,同nums[mid]>nums[j]
,此时需要移动j
的指向,j = j -1
,防止漏掉最小值。 - 返回结果
nums[i]
。
代码实现
class Solution {
public int minArray(int[] nums) {
if (null == nums || nums.length < 1) {
return -1;
}
int i = 0, j = nums.length - 1, mid;
while (i < j) {
mid = (i + j) >>> 1 ; //i + (j - i) / 2
if (nums[mid] < nums[j]) {
j = mid;
} else if (nums[mid] > nums[j]) {
i = mid + 1;
} else {
//[1,1,1,1, 1 ,1,0,1,1]
j = j - 1;
}
}
return nums[i];
}
}
- Tips
在求中间坐标时
mid=(i+j)>>>1
采用了位运算,其实跟mid=i+(j-i)/2
的效果是一样的,目的是为了防止int
溢出,但是如果写成mid=i+(j-i)>>>1
就重复了,会存在超时的风险,跟优先级有关,没有必要,更倾向于mid=i+(j-i)/2
的形式,毕竟代码是给人看的,这种写法传达的信息就是为了保护不溢出。
复杂度
时间复杂度O(log2N):特殊情况下,会退化为线性的复杂度,跟给定的数组有关。
空间复杂度O(1):未使用额外的内存空间,为常数级别。