搜索旋转排序数组[特殊二分]
Posted 白龙码~
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了搜索旋转排序数组[特殊二分]相关的知识,希望对你有一定的参考价值。
文章目录
- 搜索旋转排序数组[特殊二分]
- 一、[LeetCode 33. 搜索旋转排序数组](https://leetcode.cn/problems/search-in-rotated-sorted-array/)
- 二、[LeetCode 81. 搜索旋转排序数组 II](https://leetcode.cn/problems/search-in-rotated-sorted-array-ii/)
- 三、[LeetCode 153. 寻找旋转排序数组中的最小值](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/)
- 四、[LeetCode 154. 寻找旋转排序数组中的最小值 II](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array-ii/)
搜索旋转排序数组[特殊二分]
一、LeetCode 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 。
1、旋转的本质
简单来说,旋转就是将一个原本升序的数组,从某一个点pivot断开,将后半部分整体移至前面:
2、思路分析
首先,通过O(n)的暴力遍历的方式是最简单易理解的,但是没有利用排序数组这一条件。一般来说,看到排序搜索,直接想到的就是二分查找。如何进行二分呢?
常规的二分
对于常规的二分查找,我们通过计算mid,将数组二分成左右两个升序数组,从而推算left与right指针的移动方式。
特殊的二分
本题我们同样可以通过mid把数组分成左右两个部分,但是特殊的地方在于:原本升序的数组从pivot处旋转成了两个升序数组,左边是升序的,然后出现一个跳崖,再继续升序。因此本题要使用二分解决,需要对mid的位置进行分类讨论**[记数组为nums]**:
- mid在前半部分的升序数组中
- 如果
nums[mid]>target
,此时按道理应该将right指针向小的地方移动,但是这里可以看到,nums[mid]
左边的值是小于nums[mid]
的,后半部分数组也是小于nums[mid]
的,这时候需要target的介入:- 如果
target>=nums[0]
,即target也在前半部分,那么正常将right指针移动至mid-1。 - 如果
target<nums[0]
,即target在后半部分,那么此时向后半部分搜索,因此需要将left指针移动至mid+1。
- 如果
- 如果
nums[mid]<target
,此时按道理应该将left指针向大的地方移动。只有前半部分数组中大于mid的部分满足要求,因此将left指针移动至mid+1。
- 如果
当数组进行旋转后,原先
pivot
处变成了新数组的0
下标,后半部分的升序数组全部小于nums[0]
。因此,当nums[mid]>nums[0]
时,即可断定mid
处在新数组的前半部分。
-
mid在后半部分的升序数组中
- 如果
nums[mid]>target
,此时按道理应该将right指针向小的地方移动,这里比nums[mid]
小的只有后半部分数组中mid左边的部分了,因此将right指针移动至mid-1。 - 如果
nums[mid]<target
,此时按道理应该将left指针向大的地方移动。但是这里可以看到,nums[mid]
右边的值是大于nums[mid]
的,前半部分数组也是大于nums[mid]
的,这时候需要target的介入:- 如果
target>=nums[0]
,即target在前半部分,那么此时向前半部分搜索,因此需要将right指针移动至mid-1。 - 如果
target<nums[0]
,即target在后半部分,那么正常将left指针移动至mid+1。
- 如果
int search(vector<int>& nums, int target) int n = nums.size(); int l = 0; int r = n - 1; while (l <= r) int mid = l + ((r - l) >> 1); if (nums[mid] == target) return mid; else if (nums[mid] >= nums[0]) if (nums[mid] > target && target >= nums[0]) r = mid - 1; else l = mid + 1; else if (nums[mid] < nums[0]) if (nums[mid] < target && target < nums[0]) l = mid + 1; else r = mid - 1; return -1;
- 如果
二、LeetCode 81. 搜索旋转排序数组 II
已知存在一个按非降序排列的整数数组 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,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。
给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。
本题与上题相比只有一个改动:元素有重复。
上一题由于没有重复元素,因此可以简单地通过nums[mid]与nums[0]
的关系判断mid在前半部分还是后半部分,但是本题不能这么做,假设有以下两种情况:
同样是nums[mid]>=nums[0]
,但是图1的mid在前半部分,图2的mid在后半部分。究其原因,是因为nums[l]==nums[r]
。在这种情况下,旋转数组的转折点变得不再清晰。
为了避免这种不清晰的情况,我们在nums[l]==nums[mid] && nums[mid]==nums[r] && nums[mid]!=target
时,选择将l指针向右移,r指针向左移,通过这种最朴素的方式来缩小二分区间。
「这种方法在极端条件,比如数组元素全部是1,而target!=1时,时间复杂度退化至O(n)」。
当然,我们还可以直接通过预处理,选择一个新的start作为二分查找的起点,其中
nums[start]!=nums[n-1]
,从而使start成为新的判断mid在前半部分还是后半部分的标准。当然,由于这种方法一次只会将指针移动一次,因此效率上比前一种慢一倍。
除去这一特殊情况,其它的判断方式与上一题完全相同。
bool search(vector<int>& nums, int target)
int l = 0;
int r = nums.size() - 1;
while (l <= r)
int mid = l + ((r - l) >> 1);
if (nums[mid] == target)
return true;
if (nums[mid] == nums[l] && nums[mid] == nums[r])
++l;
--r;
else if (nums[mid] >= nums[l])
if (nums[mid] > target && target >= nums[l])
r = mid - 1;
else
l = mid + 1;
else if (nums[mid] < nums[l])
if (nums[mid] < target && target < nums[l])
l = mid + 1;
else
r = mid - 1;
return false;
三、LeetCode 153. 寻找旋转排序数组中的最小值
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
这题再次与之前的题目不一样,是要找出数组的最小元素,也就是后半部分数组的第一个元素。
由于不存在重复元素,因此可以通过nums[mid]与nums[0]的关系
判断left与right的移动方向:
-
如果
nums[mid]>nums[n-1]
,说明mid在数组的前半部分,而我们要找的是后半部分的第一个元素,因此将left移动至mid+1。注意:当旋转数组依然是升序的,比如
[0,1,2,3,4]
,那么通过nums[mid]>=nums[0]
判断mid在前半部分就会出错。因此,最好的判断mid位置的方式是将nums[mid]与nums[n-1]比较
。 -
如果
nums[mid]<=nums[n-1]
,说明mid在数组的后半部分,我们需要继续向左搜索以定位至第一个元素,但是mid可能是第一个元素,所以right只能移动至mid。
int findMin(vector<int>& nums)
int l = 0;
int r = nums.size() - 1;
while (l < r)
int mid = l + ((r - l) >> 1);
if (nums[mid] <= nums[n - 1]) // 在旋转点的右半部分
r = mid;
else
l = mid + 1;
return nums[l];
四、LeetCode 154. 寻找旋转排序数组中的最小值 II
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
本题在上题的基础上,增设了元素可重复的条件。
因此,我们仿照**[二、LeetCode 81. 搜索旋转排序数组 II]**的做法,在mid位置无法判断时暴力缩小区间即可。
int findMin(vector<int>& nums)
int n = nums.size();
int l = 0;
int r = n - 1;
while (l < r)
int mid = l + ((r - l) >> 1);
if (nums[mid] == nums[l] && nums[mid] == nums[r])
--r;
++l;
else if (nums[mid] <= nums[r]) // 后半部分
r = mid;
else // 前半部分
l = mid + 1;
return nums[l];
以上是关于搜索旋转排序数组[特殊二分]的主要内容,如果未能解决你的问题,请参考以下文章
二分查找(通过相对位置判断区间位置)--17--二分--LeetCode33搜索旋转排序数组