> 文章列表 > 【刷题之路Ⅱ】LeetCode 3381.搜索旋转排序数组ⅠⅡ

【刷题之路Ⅱ】LeetCode 3381.搜索旋转排序数组ⅠⅡ

【刷题之路Ⅱ】LeetCode 3381.搜索旋转排序数组ⅠⅡ

【刷题之路Ⅱ】LeetCode 33&81.搜索旋转排序数组Ⅰ&Ⅱ

  • 一、题目描述
  • 二、解题
    • 1、方法1——暴力法
      • 1.1、思路分析
      • 1.2、代码实现
    • 2、方法2——二分法
      • 2.1、思路分析
      • 2.2、代码实现
      • 2.3、升级到81题
        • 2.3.1、改进思路分析
        • 2.3.1、改进代码实现
    • 3、改进二分法
      • 3.1、思路分析
      • 3.2、代码实现

一、题目描述

原题连接: 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在有序的那边,那就转而搜索有序的那边:
【刷题之路Ⅱ】LeetCode 3381.搜索旋转排序数组ⅠⅡ
就将搜索区间改成[left + 1, mid - 1] (因为两端和中点都被判断过):
【刷题之路Ⅱ】LeetCode 3381.搜索旋转排序数组ⅠⅡ
否则,转而判断另一端:
【刷题之路Ⅱ】LeetCode 3381.搜索旋转排序数组ⅠⅡ
这里给出该方法的递归版本。

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题的描述中其实就只是增加了一个条件,就是可能有重复元素:
【刷题之路Ⅱ】LeetCode 3381.搜索旋转排序数组ⅠⅡ
但就是因为增加了这个条件才导致了一个不怎么好解决的问题,那就是可能会出现nums[left]和nums[mid]和nums[right]都相同的情况,例如:
【刷题之路Ⅱ】LeetCode 3381.搜索旋转排序数组ⅠⅡ
这时候如果再用像33题那样的判断方法,就无法判断出哪一端是有序或无序的。
这个时候我们其实可以继续递归判断区间[left + 1, right - 1]的,因为在nums[left]和nums[right]相同的时候,在区间[left, right]和在区间[left + 1, rihgt - 1]中查找是等价的:
【刷题之路Ⅱ】LeetCode 3381.搜索旋转排序数组ⅠⅡ
然后其他地方只需要改变一处,就是在判断左端是否有序时将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]时:
【刷题之路Ⅱ】LeetCode 3381.搜索旋转排序数组ⅠⅡ
说明mid所在的区间为较大的那部分升序,而当nums[mid] < nums[0]时,则说明mid所在的区间为较小的那一部分升序:
【刷题之路Ⅱ】LeetCode 3381.搜索旋转排序数组ⅠⅡ
所以,我们就可以这样来改进我们的算法:
当nums[mid] >= nums[0]且nums[0] < target < nums[mid]时,说明target在范围为[left, mid ]“大部分"区间里,所以执行right = mid - 1:
【刷题之路Ⅱ】LeetCode 3381.搜索旋转排序数组ⅠⅡ
当nums[mid] < nums[0]但target < nums[mid]时,则说明mid所在的区间为"小部分”,而target比nums[mid]更小,所以执行right = mid - 1:
【刷题之路Ⅱ】LeetCode 3381.搜索旋转排序数组ⅠⅡ
当nums[mid] < nums[0]且target >= nums[0]时,说明mid所在的区间为"小部分",而target所在的区间为"大部分"所以我们还是要执行right = mid - 1:
【刷题之路Ⅱ】LeetCode 3381.搜索旋转排序数组ⅠⅡ

其他情况都执行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),我们只需要用到常数级的额外空间。