示例 1:
输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4
示例 2:
输入: nums = [4,5,6,7,0,1,2], target = 3
输出: -1
问题分析
首先个人认为题目中的“旋转”可能并不直观,不利于理解,在这里旋转也可以认为是数组向右循环移动,何为循环移动,看下面
1, 2, 3, 4, 5 循环向右移动一位 为 5, 1, 2, 3, 4
1, 2, 3, 4, 5 循环向右移动两位 为 4, 5, 1, 2, 3
题目要求时间复杂度O(logN),而且数组是排序的,肯定是二分法了。但是这个数组却并不是一个完全递增的数组,他的特点是
- 数组中存在一个位置index,A = [0, index)递增序列,B = [index, size)是一个递增序列
- 递增序列A中的任意一个元素均大于递增序列B的任意一个元素,用数学表示的话就是
对于特点2可能不是很明显,理由如下:
- 题中的旋转可以认为进行了X次的向右循环移动
- 每次向右循环移动都会将最后一位数移动到最左边
- 根据 特点1 和 理由2 可知每次被移动到左边的数字为序列B中最大的元素,移动过后这个元素进入了序列A的最左边,也就是说这个元素成为了序列A中最小的元素
- 根据上述三条理由确定一个在每次循环右移之后都成立的条件:
每次循环右移到数组最左边的元素都是大于递增序列B的任何一个数的,且这个元素是递增序列A中最小的一个元素
或者可以表述为
每次循环右移到递增序列A最左边的元素都是大于递增序列B中的任何一个元素,且这个元素是递增序列A中最小的一个元素
- 所以进行了X次向右循环移动的数组依旧满足 理由4 的结论,所以 特点2 成立
解法一:数组中的最小值,对左右两边各进行一次二分查找
很明显,数组的最小值就是递增序列B的第一项。最小值的左边(不包括旋最小值)是一个递增序列,最小值的右边(包括最小值)是一个递增数列。
不是很清楚的读者可以在下图,红线圈住的值就是最小值,也可以很轻易地看出上述规律。
由于题目要求时间复杂度O(lgn),所以我们必须在O(lgn)的时间内找到最小值,具体如何找呢?肯定还是二分搜索的思想,不过要分情况考虑了。
当 nums[mid] > nums[0] 时
应该是下图所示的情况,很明显,最小是在二分点的右边,所以应该是移动左指针,即
left = mid + 1;
当 nums[mid] < nums[0] 时
应该是下图所示的情况,很明显,最小值在二分点的左边,应该移动右指针,即
right = mid - 1;
不过我们少考虑的一种情况,也就是如果整个数组是一个完全递增的数组,也就是并没有经过任何变化,那么上述方法的到的最终结果就是会出出错,所以在二分结果后需要再做一个判断,判断数组的第一位和二分查找得到的结果那个小,即
if nums[0] < nums[mid]
minIndex = 0;
else
minIndex = mid;
所以寻找旋转点的最终伪代码为:
findMin(nums)
// nums is a array
left = 0;
right = nums.length - 1;
while left <= right
mid = (left + right) / 2;
// 判断是否是最小值,也就是左右都比它大
if nums[mid] is smaller than left and right values
break;
else if nums[mid] > nums[0]
left = mid + 1;
else
right = mid - 1;
if nums[0] < nums[mid]
minIndex = 0;
else
minIndex = mid;
return midIndex;
现在我们找到了最小值了,就可以以最小值为分界点分别对左右两个递增序列进行二分查找了,所以整个程序的伪码如下:
search(nums,target)
/*
nums is a array
target is a number
*/
minIndex = findMin(nums);
left = 0;
right = minIndex - 1;
while left <= right
mid = (left + right) / 2;
if nums[mid] == target
return mid;
else if target < nums[mid]
right = mid - 1;
else
left = mid + 1;
left = minIndex;
right = nums.length - 1;
while left <= right
mid = (left + right) / 2;
if nums[mid] == target
return mid;
else if target < nums[mid]
right = mid - 1;
else
left = mid + 1;
时空复杂度分析
找出旋转点O(lgn),两次二分查找均为O(lgn),算法总时间复杂度为O(lgn)
算法使用了常数空间,空间复杂度为O(1)
解法二:直接通过二分法找出目标值
这里会稍微复杂一些,依旧先分情况讨论
当 nums[mid] > nums[0] 且 nums[mid] < target
从图上来看,二分点在目标值的左边,所以应该移动左指针,即
left = mid + 1;
当 nums[mid] > nums[0] 且 nums[mid] > target 且 nums[0] < target
从图上看,二分点在目标值右边,所以应该移动右指针,即
right = mid - 1;
当 nums[mid] > nums[0] 且 nums[mid] > target 且 nums[0] > target
从图上来看,二分点在目标值的左边,应该移动左指针,即
left = mid + 1;
当 nums[mid] < nums[0] 且 nums[mid] > target
从图上来看,二分点在目标值的右边,应该移动右指针,即
right = mid - 1;
当 nums[mid] < nums[0] 且 nums[mid] < target 且 nums[0] > target
从图上来看,二分点在目标值的左边,应该移动左指针,即
left = mid + 1;
当 nums[mid] < nums[0] 且 nums[mid] < target 且 nums[0] < target
从图上来看,二分点在目标值的右边,应该移动右指针,即
right = mid - 1;
当 nums[mid] = nums[0]
从图上来看,目标值肯定在二分点的右边(暂不考虑目标值在最左端的情况),应该移动左指针,即
left = mid + 1;
当目标值位于数组两端
在程序最开始时做判断即可
所以整个程序的伪代码应该为:
search(nums, target)
/*
nums is a array
target is a number
*/
left = 0;
right = nums.length - 1;
if nums[0] == target
return 0;
if nums[nums.length - 1] == target
return nums.length - 1;
while left <= right
mid = (left + right) / 2;
if nums[mid] == target
return mid;
else
if nums[mid] > nums[0]
if target > nums[mid]
left = mid + 1;
else
if target > nums[0]
right = mid - 1;
else
left = mid + 1;
else if nums[mid] < nums[0]
if target < nums[mid]
right = mid - 1;
else
if target < nums[nums.size() - 1]
left = mid + 1;
else
right = mid - 1;
else
left = mid + 1;
return -1;
时空复杂度分析
时间复杂度为O(lgn),空间复杂度O(1)