您现在的位置是:首页 >技术教程 >【刷题之路Ⅱ】LeetCode 33&81.搜索旋转排序数组Ⅰ&Ⅱ网站首页技术教程
【刷题之路Ⅱ】LeetCode 33&81.搜索旋转排序数组Ⅰ&Ⅱ
【刷题之路Ⅱ】LeetCode 33&81.搜索旋转排序数组Ⅰ&Ⅱ
一、题目描述
原题连接: 33. 搜索旋转排序数组 81. 搜索旋转排序数组 II
题目描述:
33题的描述如下:
整数数组 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
-104 <= nums[i] <= 104
nums 中的每个值都 独一无二
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-104 <= target <= 104
而81题只是在33题的基础上增加了有重复元素的条件。
二、解题
1、方法1——暴力法
1.1、思路分析
顺序遍历数组中所有的元素,遇到nums[i] == target返回i即可。
1.2、代码实现
有了以上思路,那我们写起代码来也就水到渠成了:
int search1(int* nums, int numsSize, int target) {
assert(nums);
int i = 0;
for (i = 0; i < numsSize; i++) {
if (nums[i] == target) {
return i;
}
}
return -1;
}
时间复杂度:O(n),n为数组长度。
空间复杂度:O(1),我们只需要用到常数级的额外空间。
当然啦,暴力法是万能的,所以对于81题这个方法根本不用做任何修改也能直接通过。
2、方法2——二分法
2.1、思路分析
看到题目中给的“有序”我们就应该想到要用二分查找法,但这里的有序并不是完全有序,而是部分有序。
而我们知道,对于完全有序的序列,我们是可以百分百的确定一个数是否在这个序列中的,那我们就可以用二分法的变种——二分搜索,该算法可以每次淘汰一半的数据,具体思路如下:
先判断nums[left]和nums[mid]和nums[right]中有没有等于target的的,如果有直接返回下标即可;
若nums[mid] != target,则应判断mid两边的区间[left, mid] 和 [mid, right]主要看有序的那边,这里优先判断左端是否有序。如果target在有序的那边,那就转而搜索有序的那边:
就将搜索区间改成[left + 1, mid - 1] (因为两端和中点都被判断过):
否则,转而判断另一端:
这里给出该方法的递归版本。
2.2、代码实现
有了以上思路,那我们写起代码来也就水到渠成了:
// 先写一个递归的二分搜索算法
int binary_search(int* nums, int left, int right, int target) {
assert(nums);
if (left > right) {
return -1;
}
int mid = left + (right - left) / 2;
if (nums[left] == target) {
return left;
}
else if (nums[mid] == target) {
return mid;
}
else if (nums[right] == target) {
return right;
}
else {
if (nums[left] < nums[mid]) {
if (target > nums[left] && target < nums[mid]) {
return binary_search(nums, left + 1, mid - 1, target);
}
else {
return binary_search(nums, mid + 1, right - 1, target);
}
}
else {
if (target > nums[mid] && target < nums[right]) {
return binary_search(nums, mid + 1, right - 1, target);
}
}
}
return binary_search(nums, left + 1, mid - 1, target);
}
int search2(int* nums, int numsSize, int target) {
assert(nums);
return binary_search(nums, 0, numsSize - 1, target);
}
时间复杂度:O(logn),n为数组长度。
空间复杂度:O(1)。
2.3、升级到81题
2.3.1、改进思路分析
我们看到81题的描述中其实就只是增加了一个条件,就是可能有重复元素:
但就是因为增加了这个条件才导致了一个不怎么好解决的问题,那就是可能会出现nums[left]和nums[mid]和nums[right]都相同的情况,例如:
这时候如果再用像33题那样的判断方法,就无法判断出哪一端是有序或无序的。
这个时候我们其实可以继续递归判断区间[left + 1, right - 1]的,因为在nums[left]和nums[right]相同的时候,在区间[left, right]和在区间[left + 1, rihgt - 1]中查找是等价的:
然后其他地方只需要改变一处,就是在判断左端是否有序时将nums[left] < nums[mid]改成nums[left] <= nums[mid]即可。
2.3.1、改进代码实现
有了以上思路,那我们写起代码来也就水到渠成了:
// 先写一个递归版的二分查找法
bool binary_search(int* nums, int left, int right, int target) {
assert(nums);
if (left > right) {
return false;
}
int mid = left + (right - left) / 2;
if (nums[left] == target) {
return true;
}
else if (nums[mid] == target) {
return true;
}
else if (nums[right] == target) {
return true;
}
else if (nums[left] == nums[mid] && nums[mid] == nums[right]) {
return binary_search(nums, left + 1, right - 1, target);
}
else if (nums[left] <= nums[mid]) {
if (target > nums[left] && target < nums[mid]) {
return binary_search(nums, left + 1, mid - 1, target);
}
else {
return binary_search(nums, mid + 1, right - 1, target);
}
} else {
if (target > nums[mid] && target < nums[right]) {
return binary_search(nums, mid + 1, right - 1, target);
}
}
return binary_search(nums, left + 1, mid - 1, target);
}
bool search(int* nums, int numsSize, int target){
assert(nums);
return binary_search(nums, 0, numsSize - 1, target);
}
时间复杂度:O(logn),n为数组长度。
空间复杂度:O(1)。
3、改进二分法
3.1、思路分析
对于33题,其实我们可以这样来改进算法:
其实我们可以通过nums[0]和nums[mid]的大小关系来判断mid所在的区间范围,当nums[mid]>nums[0]时:
说明mid所在的区间为较大的那部分升序,而当nums[mid] < nums[0]时,则说明mid所在的区间为较小的那一部分升序:
所以,我们就可以这样来改进我们的算法:
当nums[mid] >= nums[0]且nums[0] < target < nums[mid]时,说明target在范围为[left, mid ]“大部分"区间里,所以执行right = mid - 1:
当nums[mid] < nums[0]但target < nums[mid]时,则说明mid所在的区间为"小部分”,而target比nums[mid]更小,所以执行right = mid - 1:
当nums[mid] < nums[0]且target >= nums[0]时,说明mid所在的区间为"小部分",而target所在的区间为"大部分"所以我们还是要执行right = mid - 1:
其他情况都执行left = mid + 1;
3.2、代码实现
有了以上思路,那我们写起代码来也就水到渠成了:
int search3(int* nums, int numsSize, int target) {
assert(nums);
int left = 0;
int right = numsSize - 1;
int mid = 0;
while (left < right) {
mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
else if (nums[mid] >= nums[0] && nums[0] <= target && target <= nums[mid]) {
right = mid;
}
else if (nums[mid] < nums[0] && target < nums[mid]) {
right = mid;
}
else if (nums[mid] < nums[0] && target >= nums[0]) {
right = mid;
}
else {
left = mid + 1;
}
}
return left == right && nums[left] == target ? left : -1;
}
时间复杂度:O(logn),n为数组的长度。
空间复杂度:O(1),我们只需要用到常数级的额外空间。