【算法练习题】力扣练习题——数组(5): 搜索旋转排序数组

原题说明:假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [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。


总结

这道题目非常恶心。在于它给的实例有不旋转的情况,也在于我对二分查找的边界不了解。但是如果能够耐心地在边界处推导一下,也没有那么难。

  • 要注重实例的探索;
  • 要注重边界处的推导。

猜你喜欢

转载自www.cnblogs.com/RicardoIsLearning/p/12059311.html