[Leetcode 33] 搜索旋转排序数组

原本,整数数组 nums 按升序排列,数组中的值 互不相同 。
nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 向左旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
例如, [0,1,2,4,5,6,7] 下标 3 上向左旋转后变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target 。
如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
 
 
示例 1:
notion image
示例 2:
示例 3:
 

解法一:先找到旋转点,再做二分

旋转后的数组虽然整体不再有序,但它仍然保留了一个非常强的结构:由两段各自有序的升序数组拼接而成
比如:[4,5,6,7,0,1,2] = [4, 5, 6, 7] + [0, 1, 2]
最直观的做法是把问题拆成两步:
  1. 找到旋转点,也就是数组里的最小值位置
  1. 按旋转点把数组重新切成两个有序区间。接下来只需要判断 target 可能在哪个区间,然后在那个区间里做标准二分查找。
我们可以用先用二分查找找旋转点,因为最小值一定在旋转后的第二段开头。
  • 如果 nums[mid] > nums[-1],说明 mid 还在左侧较大的有序段里,最小值一定在右边。
  • 否则,nums[mid] <= nums[-1],说明 mid 已经在右侧较小的有序段里,最小值可能就mid或在它左边
复杂度分析
  • 时间复杂度是 O(log n)
    • 先用一次二分找到 pivot,
    • 再用一次二分查找目标值,
    • 总体仍然是 O(log n)
  • 空间复杂度是 O(1),只使用了常数个指针变量。

解法二:先找到 pivot,再用“虚拟下标”做二分

旋转数组的本质是 一个升序数组被整体平移了。旋转只是改变了下标,不改变值的相对升序关系。
既然如此,我们可以想象把它“转回去”,恢复成原来的升序数组。
在如同 解法一 那样先找到 pivot后,只要用取模映射下标,就可以在虚拟有序数组上做标准二分。
还是这个例子:nums = [4, 5, 6, 7, 0, 1, 2]
虚拟数组下标 mid 对应原数组下标:realMid = (mid + pivot) % n
这样我们就可以像在普通升序数组里一样做二分,只是每次比较时用 nums[realMid]
复杂度分析
  • 时间复杂度是 O(log n)
    • 一次二分找 pivot,
    • 一次二分查找 target。
  • 空间复杂度是 O(1)
    • 没有创建额外数组,只是通过取模完成下标映射。

解法三:一次二分 (推荐)

前两个解法都先找 pivot,再查 target。它们很容易理解,但还可以更进一步:不显式找 pivot,而是在每一轮二分里判断哪一半是有序的。
旋转数组虽然整体不是有序的,但任意切一刀,把区间 [left, right] 分成:[left ... mid][mid ... right]
这两半里,至少有一半一定是有序的
我们要做的事情,是在每一轮二分中安全地丢掉一半搜索空间。
  • 如果 nums[left] <= nums[mid],说明左半边有序。
    • 既然左半边有序,我们就可以通过和 nums[left] 以及 nums[mid] 两个值的比较,判断 target 是否落在这个有序区间里:nums[left] <= target < nums[mid]
      如果在,那就保留左半边;否则丢掉左半边,去右边找。
  • 反过来,说明左半边不是有序的,那么右半边一定有序
    • 此时通过和 nums[mid] 以及 nums[right] 两个值的比较,判断 target 是否落在这个有序区间里:nums[mid] < target <= nums[right]
      如果 target 落在右半边的有序区间里,就去右边;否则去左边。
这个解法在每一轮先找到有序的一半,就能通过简单的比较判断 target 是否在这一半里,也就可以丢掉另一半搜索空间。
复杂度分析
  • 时间复杂度是 O(log n)
    • 每一轮都会丢掉一半搜索区间。
  • 空间复杂度是 O(1)
    • 只使用了 leftrightmid 三个指针变量。
Buy Me a Coffee
上一篇
[Leetcode 32] 最长有效括号
下一篇
[Leetcode 34] 在排序数组中查找元素的第一个和最后一个位置