前言
所谓旋转排序数组,就是指按照升序排序的数组在预先未知的某个点上进行了旋转(例如,数组 [0,1,2,4,5,6,7]
可能变为 [4,5,6,7,0,1,2]
)。而LeetCode上与此相关的的题共有三道,下面就具体来分析一下这三题。
搜索旋转排序数组(LeetCode 33题)
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是 O(log n) 级别。
示例 1:
输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4示例 2:
输入: nums = [4,5,6,7,0,1,2], target = 3
输出: -1来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
分析
从题目要求时间复杂度为O(logn),则可知最终必须使用二分法。然后再具体分析旋转数组的特征,假设在下标i
处进行了旋转,旋转后的序列为i ~ n-1, 0 ~ i-1
,由于数组本身是升序排列的,所以仔细分析mid
中间结点的位置,便可以找到这题的突破口,假设mid
处于i ~ n-1
区间内,那么i ~ mid
段就是有序的,相对地,mid ~ i-1
就是无序的。当mid
结点处于0 ~ i-1
区间时,易知mid ~ i-1
段是有序的,相对地,i ~ mid
就是无序的。可以发现,无论mid在哪个位置,总能得到一个有序段,最终我们也就是利用有序段,来每次排除一半的数据。
为了分析有序段,再看题目,因为数组为升序,因此我们每次只需要和最右边的结点进行比较即可。若nums[mid] < nums[right]
,则mid ~ right
段为有序,若nums[mid] > nums[right]
,由于数组整体为升序,原数组中左边的数据一定是小于右边的,因此在mid ~ right
存在一个旋转点,即该段是无序的,再由上文分析可知,无论mid
处于哪一段,总存在有序区间,因此此时i ~ mid
段是有序的。
当我们得到有序段后,我们便可以根据target
是否存在于有序段来取舍一半的数据,最终的代码如下:
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = (left + right) >>> 1;
if (nums[mid] == target) {
return mid;
}
if (nums[mid] > nums[right]) {
// target是否存在于left ~ mid有序段
if (nums[left] <= target && nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
// target是否存在于mid ~ right有序段
if (nums[right] >= target && nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
复杂度分析
- 时间复杂度:每次取舍一半的数据,为二分查找,故为O(logn);
- 空间复杂度:未使用额外空间,为O(1)。
寻找旋转排序数组中的最小值(LeetCode153题)
分析
本题的条件和上一题相同,取别是上题要求找到一个目标值,而本题则要求找到最小值。再次回想之前的分析,假设在下标i
处进行了旋转,旋转后的序列为i ~ n-1, 0 ~ i-1
,对于mid
结点而言,由之前的分析可知,可以由mid
结点与最右边的结点的值的大小关系,判断出有序区间处于mid
之前还是之后。再次分析有序区间,假设i ~ mid
段为有序,那么数组一定是在mid ~ i-1
段的某个位置进行了旋转,相应地,最小值也存在于这个区间,即存在于无序段。当mid ~ i-1
为有序时,则根据上文的分析,最小值也一定存在于存在旋转结点的区间,即无序段。
经过上文的分析,可知最小值(也就是旋转结点)一定存在于无序段,因此代码也很容易得出,如下:
public int findMin(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int mid = (left + right) >>> 1;
// mid > right, 则mid必定不为最小值,
// 结合left ~ mid为有序段,故可直接舍弃left ~ mid段
if (nums[mid] > nums[right]) {
left = mid + 1;
} else {
// mid < right, mid处可能为最小值,
// 因此对于有序段mid ~ right, 只能舍弃mid+1 ~ right
right = mid;
}
}
return nums[left];
}
复杂度分析
- 时间复杂度:O(logn);
- 空间复杂度:O(1)。
存在重复值的数组
以上两题均是不考虑数组有重复元素的情况,而LeetCode81题和154题,则正是以上两题存在重复元素的改变。当存在重复元素时,我们在判断mid
与right
结点的大小也将多了一种可能相等的情况,那么怎么考虑相等呢?其实我们只要每次在相等情况判断时,都只将right--
即可,因为不管是寻找目标值target
,还是寻找数组最小值,因为mid
与right
相等,所以每次去掉最右结点,再对剩下部分之前的判断,即可得到最终的结果。但是,在有重复值的情况下,算法的时间复杂度也有了改变,在最好的情况下(数组中不存在重复元素),仍未O(logn)。但在最坏的情况下(数组元素全部相等),由于每次只能将数组大小缩减1,因此将退化为O(n)。