[LeetCode] 搜索旋转排序数组

示例 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),而且数组是排序的,肯定是二分法了。但是这个数组却并不是一个完全递增的数组,他的特点是

  1. 数组中存在一个位置index,A = [0, index)递增序列,B = [index, size)是一个递增序列
  2. 递增序列A中的任意一个元素均大于递增序列B的任意一个元素,用数学表示的话就是

对于特点2可能不是很明显,理由如下:

  1. 题中的旋转可以认为进行了X次的向右循环移动
  2. 每次向右循环移动都会将最后一位数移动到最左边
  3. 根据 特点1理由2 可知每次被移动到左边的数字为序列B中最大的元素,移动过后这个元素进入了序列A的最左边,也就是说这个元素成为了序列A中最小的元素
  4. 根据上述三条理由确定一个在每次循环右移之后都成立的条件:

    每次循环右移到数组最左边的元素都是大于递增序列B的任何一个数的,且这个元素是递增序列A中最小的一个元素

或者可以表述为

每次循环右移到递增序列A最左边的元素都是大于递增序列B中的任何一个元素,且这个元素是递增序列A中最小的一个元素

  1. 所以进行了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;

当目标值位于数组两端

在程序最开始时做判断即可

未完待续

猜你喜欢

转载自www.cnblogs.com/FDProcess/p/10614145.html