搜索旋转排序数组
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -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
搜索旋转数组中最小的数
在寻求上面题目的解时,我们先简化一下题目,看一下这一道。
这道题最直观的做法并不难,从头到尾遍历数组一次,我们就能找出最小的元素。这种思路的时间复杂度显然是O(n)。但是这种思路没有利用输入的旋转数组的特性。
我们注意到旋转之后的数组实际上可以划分为两个排序的子数组,而且前面的子数组的元素都大于或者等于后面子数组的元素。还注意到最小的元素刚好是这两个子数组的分界线。在排序的数组中我们可以用二分查找法实现O(logn)的查找。本题给出的数组在一定程度上是排序的,因此哦们可以试着用二分查找法的思路来寻找这个最小的元素。
和二分查找法一样,我们用两个指针分别指向数组的第一个元素和最后一个元素。按照题目中旋转的规则,第一个元素应该是大于或者等于最后一个元素的(这其实不完全对,还有特例)。
接着我们可以找到数组中间的元素。如果该中间的元素位于前面的递增子数组,那么它应该大于或者等于第一个指针指向的元素。此时数组中最小的元素应该位于该中间元素的后面。我们可以把第一个指针指向该中间元素,这样就可以缩小寻找的范围。移动之后的第一个指针仍然位于前面的递增子数组。
同样,如果中间元素位于后面的递增子数组,那么它应该小于或者等于第二个指针指向的元素。此时该数组中最小的元素应该位于该中间元素的前面。我们可以把第二个指针指向该中间元素,这样也可以缩小寻找的范围。移动之后的第二个指针仍然位于后面的递增子数组。
按照上面的思路,第一个指针总是指向前面递增数组的元素,而第二个指针总是指向后面递增数组的元素。最终第一个指针将指向前面子数组的最后一个元素,而第二个指针会指向后面子数组的第一个元素。也就是它们最终会指向两个相邻的元素,而第二个指针指向的刚好是最小的元素。这就是循环结束的条件。
前面我们提到,在旋转数组中,由于是把递增排序数组前面的若干个数字搬到数组的后面,因此第一个数字总是大于或者等于最后一个数字。但按照定义还有一个特例:如果把排序数组的前面的0个元素搬到最后面,即排序数组本身,这仍然是数组的一个旋转,我们的代码需要支持这种情况。此时,数组中的第一个数字就是最小的数字,可以直接返回。
我们再来看一个例子。数组{1,0,1,1,1}和数组{1,1,1,0,1}都可以看成递增排序数组{0,1,1,1,1}的旋转。
在这两种情况中,第一个指针和第二个指针指向的数字都是1,并且两个指针中间的数字也是1,这三个数字相同。在第一种情况中,中间数字位于后面的子数组;在第二种情况中,中间数字位于前面的子数组。因此,当两个指针指向的数字及它们中间的数字三者相同的时候,我们无法判断中间的数字是位于前面的子数组还是后面的子数组,也就无法移动两个指针来缩小查找的范围。此时我们需要顺序查找。
public int searchMin(int[] nums){
// 搜索旋转矩阵中最小的
if (nums.length == 0){
return 0;
}
int low = 0;
int high = nums.length - 1;
int mid = 0;
while (nums[low] >= nums[high]){
if (high - low == 1){
mid = high;
break;
}
mid = (low+high) / 2;
if (nums[low]== nums[high] && nums[mid] == nums[low]){
return minInOrder(nums,low,high);
}
if (nums[mid] > nums[low]){
low = mid;
} else if (nums[mid] < nums[high]){
high = mid;
}
}
return nums[mid];
}
private int minInOrder(int[] numbers,int index1,int index2){
int result = numbers[index1];
for(int i = index1+1;i<=index2;++i){
if (result > numbers[i]){
result = numbers[i];
}
}
return result;
}
搜索旋转排序数组
下面我们回到这个问题上,如何搜索旋转排序数组。这道题最直观的做法并不难,从头到尾遍历数组一次,我们就能找出目标的元素。这种思路的时间复杂度显然是O(n)。但是这种思路没有利用输入的旋转数组的特性。
同样的,我们注意到旋转之后的数组实际上可以划分为两个排序的子数组,而且前面的子数组的元素都大于或者等于后面子数组的元素。在排序的数组中我们可以用二分查找法实现O(logn)的查找。本题给出的数组在一定程度上是排序的,因此可以试着用二分查找法的思路来寻找这个目标的元素。
和二分查找法一样,我们用两个指针分别指向数组的第一个元素和最后一个元素。按照题目中旋转的规则,第一个元素应该是大于或者等于最后一个元素的(这其实不完全对,还有特例)。
对于数组nums = [4,5,6,7,0,1,2], target = 1,我们先找到这个数组中最小的元素的下标,也就是找到这个数组的分界点,接着判断target是属于数组的左半部分还是右半部分。接着直接使用二分查找就可以完成任务。
class Solution {
public int search(int[] nums, int target) {
if (nums.length == 0){
return -1;
}
if (nums.length == 1){
if (nums[0] == target){
return 0;
} else{
return -1;
}
}
int mid = searchMinIndex(nums); // 旋转的分界点
if (mid == 0){
return searchTwo(nums,target,0,nums.length-1);
}
if (target < nums[mid]){
return -1;
} else if (target>=nums[mid] && target<=nums[nums.length-1]){
return searchTwo(nums,target,mid,nums.length-1);
} else if (target>=nums[0] && target<=nums[mid-1]){
return searchTwo(nums,target,0,mid-1);
} else{
return -1;
}
}
private int searchTwo(int[] nums,int target,int lo,int hi) {
while (lo <= hi){
int mid = lo + (hi - lo) / 2;
if (nums[mid] > target){
hi = mid-1;
} else if (nums[mid] < target){
lo = mid+1;
} else{
return mid;
}
}
return -1;
}
public int searchMin(int[] nums){
// 搜索旋转矩阵中最小的
int low = 0;
int high = nums.length - 1;
int mid = 0;
while (nums[low] >= nums[high]){
if (high - low == 1){
mid = high;
break;
}
mid = (low+high) / 2;
if (nums[low]== nums[high] && nums[mid] == nums[low]){
return minInOrder(nums,low,high);
}
if (nums[mid] > nums[low]){
low = mid;
} else if (nums[mid] < nums[high]){
high = mid;
}
}
return nums[mid];
}
public int searchMinIndex(int[] nums){
int low = 0;
int high = nums.length - 1;
int mid = 0;
while (nums[low] >= nums[high]){
if (high - low == 1){
mid = high;
break;
}
mid = (low+high) / 2;
if (nums[low]== nums[high] && nums[mid] == nums[low]){
return minInOrder(nums,low,high);
}
if (nums[mid] > nums[low]){
low = mid;
} else if (nums[mid] < nums[high]){
high = mid;
}
}
return mid;
}
private int minInOrderIndex(int[] numbers,int index1,int index2){
int result = numbers[index1];
int i = index1+1;
for(;i<=index2;++i){
if (result > numbers[i]){
result = numbers[i];
}
}
return i;
}
private int minInOrder(int[] numbers,int index1,int index2){
int result = numbers[index1];
for(int i = index1+1;i<=index2;++i){
if (result > numbers[i]){
result = numbers[i];
}
}
return result;
}
}