原题说明:假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。你可以假设数组中不存在重复的元素。你的算法时间复杂度必须是 O(log n) 级别。
原题链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array
题目分析:
从时间复杂度上看,满足条件的解题算法应该就是二分查找。牛人有自己的快捷步骤,但是我的想法是分步骤、慢慢来。
后来在测试一个实例时,我才知道二分查找是基于数列有序的前提下,所以对于这样一个“旋转”的数组,需要先找到旋转点,然后以旋转点为参考,选中其中一段进行二分查找。
换言之,题目设置的“旋转”障碍,就是将一个有序数组分成了两段,使得直接使用二分查找不可行。
故解题也分成两部分讨论:
PART1→找到旋转点Rotation index
PART2→根据旋转点找到可能对应的区间(因为可能不存在),再进行二分查找
PART1:找到旋转点
这次直接分析源码
1 private int find_rotation(int[] nums, int left, int right) { 2 if(nums[left]<nums[right]) 3 return 0; 4 5 int rotation; 6 int mid = (left+right+1)>>1; 7 while(left<right) { 8 if(nums[mid]<nums[mid-1]) 9 return mid - 1; 10 else { 11 if(nums[mid]<nums[left]) { 12 right = mid - 1; 13 mid = (left+right+1)>>1; 14 } 15 else { 16 left = mid; 17 mid = (left+right+1)>>1; 18 } 19 } 20 } 21 return 0; 22 }
1. 根据实际测试,数组$nums$是有可能不旋转的,即整体是一个有序数列(递增数列)。在这样的情况下,旋转点不存在、Rotation index为0(这也是通过实例测试得知的)。故有了第2、3行代码。
2. 第6行分析。这应该算是我第2次在力扣上做二分查找了,第6行的代码也可以写成这样int mid = left + (right - left + 1) / 2;这样的话,就保障查找的时候靠近左端(整体查找时不会越界)。那么写成第6行的格式,是因为位运算效率更高(我也不知道如何得出这样结论的),其中$>>1$作为右移位运算符,由于二进制,右移一位就相当于除于2。
3. 对于循环条件,可以这样理解,当$left=0, right=1$时,$mid=(0+1+1)/2=1$,此时$target$只可能等于或者小于(递增数列)$n u m s[m i d]$。若小于,就只能让$mid$取$left$,唯一的办法就是$right=mid-1$,此时$left==right$;或者直接终止循环,比较$target$和$n u m s[l e f t]$的大小(本题采用第二种)。也就是说,$left<right$是可以取到所有情况的,且不会数组越界。
4.第8、9行原来是这么写的:
if(nums[mid]>nums[mid+1])//7,8,9,1,2,3 return mid;
从实例上看,当然没错,可要命的是出现两个元素的数组。同3中说的,此时$mid==right=1$,如果再出现$mid+1$,就会数组越界。所以要反过来写判断条件,不过要注意,这样跟着返回值也应该修改。
5.最后是两端移动。由于左端固定,右端如果每次取值都是$mid$,就会发生3中的情况,最后$mid$和$right$都是1,陷入死循环。考虑到每次循环中,$mid$对应的值都已经被比较过了,所以取$right = mid - 1$。
6.这里有一个问题,之前3中说到,最后要比较$target$和$n u m s[l e f t]$的大小,那么为什么循环结束后都没有体现呢?因为找旋转点是二分查找的变形,如果$left$是旋转点,那么必然有$nums[mid]<nums[mid-1]==nums[left]$。换言之,此时,就已经满足了第8行的判断条件,返回旋转点$mid-1$(此时$left<mid$,所以不会越界)。而当$right==left$时,已经说明数列是递增的、或者是单元素数列,所以直接返回$0$即可。
PART2:二分查找
先看代码
1 private int binary_search(int[] nums, int left, int right, int target) { 2 int index; 3 int mid = (left+right+1)>>1; 4 while(left<right) { 5 if(nums[mid]==target) 6 return index = mid; 7 else if(nums[mid]>target) { 8 right = mid-1; 9 mid = (left+right+1)>>1; 10 } 11 else if(nums[mid]<target) { 12 left = mid; 13 mid = (left+right+1)>>1; 14 } 15 } 16 return index=(target == nums[left])?left:-1;//left is close, which makes the mid cannot get the value of left 17 }
关于二分查找的解释,这里就不再说明。主要看第16行,这里面就是PART1第3点说到的情况。比较完$nums[left]$和$target$,就遍历完所有情况了。此时若没有找到,就返回$-1$。
主函数:
1 public int solution(int[] nums, int target) { 2 if(nums.length<1 || nums == null) 3 return -1; 4 5 int left = 0, right = nums.length - 1, mid = (left+right+1)>>1; 6 int rotation = 0; 7 int index; 8 9 rotation = find_rotation(nums, left, right); 10 11 if(target>=nums[0]&&target<=nums[rotation]) 12 index = binary_search(nums, 0, rotation, target); 13 else 14 index = binary_search(nums, rotation, nums.length-1, target); 15 return index; 16 }
第9行就是PART1,第11行到第14行就是PART2。
总结:
这道题目非常恶心。在于它给的实例有不旋转的情况,也在于我对二分查找的边界不了解。但是如果能够耐心地在边界处推导一下,也没有那么难。
- 要注重实例的探索;
- 要注重边界处的推导。