LeetCode 33. 搜索旋转排序数组(Java 语言实现)

116 阅读1分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

一、题意
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/search-in-rotated-sorted-array

  整数数组 nums 按升序排列,数组中的值 互不相同

  在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如,[0, 1, 2, 4, 5, 6, 7] 在下标 3 处经旋转后变为 [4, 5, 6, 7, 0, 1, 2]。

  给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target,则返回它的下标,否则返回 -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

  示例 3:

输入:nums = [1], target = 0
输出:-1

  提示:

  • 1 <= nums.length <= 5000
  • -10^4^ <= nums[i] <= 10^4^
  • nums 中的每个值都 独一无二
  • 题目数据保证 nums 在预先未知的某个下标上进行了旋转
  • -10^4^ <= target <= 10^4^
二、算法思路
2.1 寻找分界点,在两侧数组进行二分查找

  由题意可知,数组在某个下标处进行了旋转,旋转后的数组两侧均为升序排列的有序序列。旋转后的数组存在下标 index,index 为右侧子数组的起始索引,且 nums[index] 是整个数组中的最小值,只要找到第一个小于 nums[0] 的下标,则找到了数组的分界点 index。特殊的,如果旋转后的数组整体呈现递增趋势,则可以视为 index = 0,认为原数组在下标 0 处进行了旋转。找到分界点后,根据两个子数组的大小关系,再进一步判断需要在哪个子数组中进行二分查找来寻找目标值。

  Java 版本实现代码如下所示。

package com.example.demo.test;

public class Search {

    // 33. 搜索旋转排序数组
    public static int search(int[] nums, int target) {
        // 寻找数组分界点,然后再二分查找
        if (nums.length == 1) {
            return nums[0] == target ? 0 : -1;
        }

        // 定义初始值
        int startNum = nums[0];
        // 寻找数组中第一个小于初始值的下标(若有),即得到数组的分界点
        int index = 0;
        int l = 0;
        int r = nums.length - 1;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (nums[mid] >= startNum) {
                l = mid + 1;
            } else if (mid > 0 && nums[mid - 1] < nums[mid]) {
                r = mid - 1;
            } else {
                index = mid;
                break;
            }
        }

        // index 下标是最小值
        // 如果 index = 0
        if (index == 0) {
            if (nums[0] > target || nums[nums.length - 1] < target) {
                return -1;
            } else {
                return partSearch(nums, 0, nums.length - 1, target);
            }
        }
        
        // 如果 index != 0
        // 如果最大值小于 target,或者最小值大于 target,则肯定没有目标值
        if (nums[index - 1] < target || nums[index] > target) {
            return -1;
        } else if (startNum > target) {
            // 在右侧区间二分查找
            return partSearch(nums, index, nums.length - 1, target);
        } else {
            // 在左侧区间二分查找
            return partSearch(nums, 0, index - 1, target);
        }
    }

    public static int partSearch(int[] nums, int left, int right, int target) {

        while (left <= right) {
            int mid = (left + right) / 2;

            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return -1;
    }
}

2.2 不寻找分界点,直接在原数组进行二分查找

  对于有序数组,可以使用二分查找的方法查找目标元素。

  但是这道题中,数组本身不是有序的,进行旋转后只保证了数组的局部有序性,这还能进行二分查找吗?答案是可以的。

  可以发现的是,我们将数组从中间分开成左右两部分的时候,一定有一部分的数组是有序的。拿示例来看,我们从 6 这个位置分开以后数组变成了 [4, 5, 6] 和 [7, 0, 1, 2] 两个部分,其中左边 [4, 5, 6] 这个部分的数组是有序的,其他数组分割后也是如此。

  这启示我们可以在常规二分查找的时候查看当前以 mid 为分割位置分割出来的两个部分 [l, mid] 和 [mid + 1, r] 哪个部分是有序的,并根据有序的那个部分确定我们该如何改变二分查找的上下界,因为我们能够根据有序的那部分判断出 target 在不在这个部分:

  • 如果 [l, mid - 1] 是有序数组,且 target 的大小满足 [nums[l], nums[mid]),则我们应该将搜索范围缩小至 [l, mid - 1],否则在 [mid + 1, r] 中寻找。
  • 如果 [mid, r] 是有序数组,且 target 的大小满足 (nums[mid+1],nums[r]],则我们应该将搜索范围缩小至 [mid + 1, r],否则在 [l, mid - 1] 中寻找。

在这里插入图片描述

  需要注意的是,二分查找的写法有很多种,所以在判断 target 大小与有序部分的关系时,可能会出现细节上的差别。

  Java 语言版本的实现代码参考如下。

	// 33. 搜索旋转排序数组
    public static int search2(int[] nums, int target) {
        // 不找分界点,直接二分查找
        if (nums.length == 1) {
            return nums[0] == target ? 0 : -1;
        }

        int l = 0;
        int r = nums.length - 1;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (nums[mid] == target) {
                return mid;
            }
            if (nums[0] <= nums[mid]) {
                // 说明 mid 在旋转数组的左侧有序区间内
                if (nums[mid] > target && nums[0] <= target) {
                    // 在左侧区间查找
                    r = mid - 1;
                } else {
                    // 在右侧区间查找
                    l = mid + 1;
                }
            } else {
                // 说明 mid 在旋转数组的右侧有序区间内
                if (nums[mid] < target && nums[nums.length - 1] >= target) {
                    // 在右侧区间查找
                    l = mid + 1;
                } else {
                    // 在左侧区间查找
                    r = mid - 1;
                }
            }
        }
        return -1;
    }