本文已参与「新人创作礼」活动,一起开启掘金创作之路。
一、题意
来源:力扣(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;
}