搜索旋转排序数组[特殊二分]
整数数组 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;
}
已知存在一个按非降序排列的整数数组 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;
}
已知一个长度为 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];
}
已知一个长度为 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];
}
|