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

假设按照升序排序的数组在预先未知的某个点上进行了旋转。

( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。

搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。

你可以假设数组中不存在重复的元素。

你的算法时间复杂度必须是 O(log n) 级别。

示例 1:

输入: nums = [4,5,6,7,0,1,2], target = 0

输出: 4

示例 2:

输入: nums = [4,5,6,7,0,1,2], target = 3

输出: -1

这道题,解题的关键在于找到旋转的起始索引位置,之后的解题方式就是普通的二分查找有序数组了。

# 解法一

由于旋转前数组有序,因此只要我们在遍历数组时,找到前一个数比后一个数大的情况,即找到了旋转起始点。

  let start = 0;
  for (let i = 1; i < nums.length; i++) {
    if (nums[i] < nums[i - 1]) {
      start = i;
      break;
    }
  }

该起始点为中心,将数组分为了两个升序数组,那么我们只需要判断target在哪一侧数组,即可确定二分搜索的上下界,从而完成解题。

  if (target === nums[0]) {
    // 如果target恰好等于第一个,则直接返回0索引
    return 0;
  } else if (target > nums[0] && start !== 0) {
    // 当target大于最左侧元素,此时target在左半边序列中查询。
    // 注意,当start=0时,此时没有左半边序列,只能从右半边序列中查询
    left = 0;
    right = start - 1;
  } else {
    // target在右半边序列中查询
    left = start;
    right = nums.length - 1;
  }

确定了上下界,二分搜索求结果的方法就非常常规了,不理解的可以去看看专题中二分搜索的解法介绍。

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (nums[mid] < target) {
      left = mid + 1;
    } else if (nums[mid] > target) {
      right = mid - 1;
    } else {
      return mid;
    }
  }
  return -1;

至此,题目就AC通过了。

但是,由于题目要求时间复杂度为O(lgn),因此,查找旋转起点的方式也必须使用二分法。

  let left = 0,
    right = nums.length,
    start = 0;
  while (left < right) {
    const mid = Math.floor((left + right) / 2);
    if (nums[mid] > nums[mid + 1]) {
      // 说明此时找到了旋转位置,位置为 mid + 1
      start = mid + 1;
      break;
    } else {
      // 通过二分缩小查询范围
      if (nums[mid] > nums[left]) {
        left = mid + 1;
      } else {
        right = mid;
      }
    }
  }

时间总复杂度为O(2lgn),符合要求。

function search(nums: number[], target: number): number {
  let left = 0,
    right = nums.length,
    start = 0;
  while (left < right) {
    const mid = Math.floor((left + right) / 2);
    if (nums[mid] > nums[mid + 1]) {
      // 说明此时找到了旋转位置,位置为 mid + 1
      start = mid + 1;
      break;
    } else {
      // 通过二分缩小查询范围
      if (nums[mid] > nums[left]) {
        left = mid + 1;
      } else {
        right = mid;
      }
    }
  }

  if (target === nums[0]) {
    // 如果target恰好等于第一个,则直接返回0索引
    return 0;
  } else if (target > nums[0] && start !== 0) {
    // 当target大于最左侧元素,此时target在左半边序列中查询。
    // 注意,当start=0时,此时没有左半边序列,只能从右半边序列中查询
    left = 0;
    right = start - 1;
  } else {
    // target在右半边序列中查询
    left = start;
    right = nums.length - 1;
  }

  // 正常二分查找
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (nums[mid] < target) {
      left = mid + 1;
    } else if (nums[mid] > target) {
      right = mid - 1;
    } else {
      return mid;
    }
  }
  return -1;
}

# 解法二

我们也可以换一种思路来解题。我们注意到,由于有序数组只旋转一次,那么对于任意一点,一定有左侧或者右侧符合单调递增,另一侧不符合。

可以利用这一特性在一次遍历中二分缩小查找比对范围。

var search = function(nums, target) {
  if (!nums || nums.length === 0) return -1;

  let left = 0;
  let right = nums.length - 1;
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (target === nums[mid]) {
      return mid;
    } else if (nums[mid] >= nums[left]) {
      // 说明[left, mid]之间是有序的
      if (target < nums[mid] && target >= nums[left]) {
        // 因为[left, mid]有序,若target在左侧范围内,则一定会大于nums[left]小于nums[mid]
        right = mid - 1;
      } else {
        // 反之一定不在该本次二分的有序范围内,递归右侧
        left = mid + 1;
      }
    } else if (nums[mid] <= nums[right]) {
      // 说明[mid, right]之间是有序的
      if (target > nums[mid] && target <= nums[right]) {
        left = mid + 1;
      } else {
        right = mid - 1;
      }
    }
  }
  return -1;
};