有序旋转数组是指将有序数组向左或者向右移动k个位置得到的结果,其查找算法不难理解,因为局部有序,因此很容易想到二分查找是最合适的方法,时间复杂度O(nlogn),本文总结四道相关的算法题目。
(一)旋转数组
题目:189. 旋转数组
题目描述:
给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
示例 1:
输入: [1,2,3,4,5,6,7] 和 k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]
解题思路:三次反转
实际上本题不难想到解答,也是在面试中很常见的一个题目,实际上它类似于【剑指Offer】43、左旋转字符串,对于左旋转,可以分为前k个元素和其他元素,而右旋转可以分为后k个元素和前面的其他元素,仍然可以借鉴三次反转的算法来实现。
代码思路:
class Solution {
public void rotate(int[] nums, int k) {
//右移k位,三次反转
if(nums==null || nums.length==0 || k==0)
return ;
int len=nums.length;
k=k%len;
reverse(nums,len-k,len-1);
reverse(nums,0,len-k-1);
reverse(nums,0,len-1);
}
//数组反转
public void reverse(int[] nums,int begin,int end){
int i=begin,j=end;
while(i<j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
i++;
j--;
}
}
}
(二)有序数组的查找
在有序数组中查找一个特定的值target,这就是典型的二分查找算法,其过程为:
- 若 target == nums[mid],直接返回
- 若 target < nums[mid],则 target 位于左侧区间 [left,mid) 中。令 right = mid-1,在左侧区间查找
- 若 target > nums[mid],则 target 位于右侧区间 (mid,right] 中。令 left = mid+1,在右侧区间查找
public boolean search(int[] nums, int target) {
if(nums==null || nums.length==0)
return false;
int low=0,high=nums.length-1;
while(low<=high){
int mid=low+(high-low)/2;
if(nums[mid]==target)
return true;
else if(target<nums[mid]){
high=mid-1;
}else{
low=mid+1;
}
}
return false;
}
(三)搜索旋转排序数组
题目:33. 搜索旋转排序数组
题目描述:
假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。你可以假设数组中不存在重复的元素。你的算法时间复杂度必须是 O(log n) 级别。
示例:
输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4
输入: nums = [4,5,6,7,0,1,2], target = 3
输出: -1
解题思路:二分查找
对于本题,二分查找是自然而然可以想到的思路,特别是更要求算法时间复杂度是O(logn),那几乎二分查找就成为了唯一的选择。但是,由于本题并不是完全有序,而是经过旋转后的局部有序。
因此,对于这种局部有序的数组,我们可以观察到一个特点:从任一位置进行分隔,则必有其中一半是有序的。因此,我们可以仍然从中点位置进行分隔,然后通过将最左边元素nums[low]和nums[mid]进行比较,从而可以判断出来,是左侧还是右侧是连续有序的,进而可以选择接下来继续选择哪一边进行继续查找。
1、若 target == nums[mid],直接返回
2、若 nums[left] <= nums[mid],说明左侧区间 [left,mid]「连续递增」。此时:
若 nums[left] <= target <= nums[mid],说明 target 位于左侧。令 right = mid-1,在左侧区间查找
否则,令 left = mid+1,在右侧区间查找
3、否则,说明右侧区间 [mid,right]「连续递增」。
此时:
若 nums[mid] <= target <= nums[right],说明 target 位于右侧区间。令 left = mid+1,在右侧区间查找
否则,令 right = mid-1,在左侧区间查找
代码实现:
class Solution {
public int search(int[] nums, int target) {
if(nums==null || nums.length==0)
return -1;
int low=0,high=nums.length-1;
while(low<=high){
int mid=low+(high-low)/2;
if(nums[mid]==target)
return mid;
else if(nums[low]<=nums[mid]){ //左侧连续
if(target<nums[mid] && target>=nums[low])
high=mid-1;
else
low=mid+1;
}else{ //右侧连续
if(target>nums[mid] && target<=nums[high])
low=mid+1;
else
high=mid-1;
}
}
return -1;
}
}
(四)搜索旋转排序数组II
题目描述:
假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。
示例 :
输入: nums = [2,5,6,0,0,1,2], target = 0
输出: true
解题思路:
本题是上一题的延续,其区别在于数组中是否包含重复元素,当数组中允许元素重复时,如果nums[low]和nums[mid]是相等的,那我们就无法判断究竟是哪一边连续有序,这时我们可以让指针只移动一位,相当于排除一个元素。其余和上题基本相同。
代码实现:
class Solution {
public boolean search(int[] nums, int target) {
if(nums==null || nums.length==0)
return false;
int low=0,high=nums.length-1;
while(low<=high){
int mid=low+(high-low)/2;
if(nums[mid]==target)
return true;
if(nums[low]==nums[mid]){
low++;
}else if(nums[low]<nums[mid]){ //左边有序
if(target>=nums[low] && target<nums[mid])
high=mid-1;
else
low=mid+1;
}else{ //右边有序
if(target>nums[mid] && target<=nums[high])
low=mid+1;
else
high=mid-1;
}
}
return false;
}
}
总结:
本文总结了关于旋转排序数组的四道题目,重点是二分查找算法的变形应用,是一个重要的算法知识点。