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) 的算法解决此问题。
示例
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
题解
二分法
对于有序数组,可以使用二分查找的方法查找元素。
但是这道题中,数组本身不是有序的,进行旋转后只保证了数组的局部是有序的。
我们将数组从中间分开成左右两部分的时候,一定有一部分的数组是有序的。
这启示我们可以在常规二分查找的时候查看当前 mid 为分割位置分割出来的两个部分 [l, mid] 和 [mid + 1, r] 哪个部分是有序的,并根据 target 在有序的那个部分的大小比较来确定我们该如何改变二分查找的上下界,因为我们能够根据有序的那部分判断出 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] 中寻找。
代码中判断部分的 大小判断 部分的 left 和 right 可以用 0 和 n-1 代替,效果相同,都是用来确定有序部分在哪边。
- 时间复杂度: O(logn),其中 n 为 nums 数组的大小。整个算法时间复杂度即为二分查找的时间复杂度 O(logn)。
- 空间复杂度: O(1) 。我们只需要常数级别的空间存放变量。
class Solution {
public:
int search(vector<int>& nums, int target) {
int n = nums.size();
int left = 0, right = n-1;
while(left <= right){
int mid = (right + left) >> 1;
if(nums[mid] == target) return mid; // 目标索引
if(nums[left] <= nums[mid]){
// 左半边有序,<=
if(target >= nums[left] && target < nums[mid]){
// target 在左半边,mid不是目标索引,因此不需要=判断,下同
right = mid - 1;
}else{
left = mid + 1;
}
}else{
// if(nums[0] > nums[mid]) // 右半边有序
if(target > nums[mid] && target <= nums[right]){
// target 在右半边
left = mid + 1;
}else{
right = mid - 1;
}
}
}
return -1;
}
};
81. 搜索旋转排序数组 II ●●
描述
整数数组 nums 按升序排列,数组中的值 可能存在重复值 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回 true,否则返回 false 。
示例
输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true
题解
二分法
对于数组中有重复元素的情况,二分查找时可能会有 a[l]=a[mid]=a[r],此时无法判断区间 [l,mid] 和区间 [mid+1,r] 哪个是有序的。
例如 nums=[3,1,2,3,3,3,3],target=2,首次二分时无法判断区间 [0,3] 和区间 [4,6] 哪个是有序的。
对于这种情况,我们只能将当前二分区间的左边界加一,右边界减一,然后在新区间上继续二分查找。
class Solution {
public:
bool search(vector<int>& nums, int target) {
int n = nums.size();
int left = 0, right = n-1;
while(left <= right){
int mid = (left + right) >> 1;
if(nums[mid] == target) return true;
if(nums[left] == nums[mid] && nums[mid] == nums[right]){
// 无法判断有序部分
++left;
--right;
}else if(nums[left] <= nums[mid]){
// 左边有序
if(target >= nums[left] && target < nums[mid]){
// 有序部分判断,target 在左半边
right = mid - 1;
}else{
left = mid + 1;
}
}else{
// 右边有序
if(target > nums[mid] && target <= nums[right]){
// 有序部分判断,target 在右半边
left = mid + 1;
}else{
right = mid - 1;
}
}
}
return false;
}
};
while (left < right)
#include <vector>
using std::vector;
// 二分寻找
//
// 解题思路:
// 第一类
// 10111和11101这种。此种情况下 "nums[start] == nums[mid]",分不清到底是前面有序还是后面有
// 序,此时! start++即啊。相当于去掉一个重复的干扰项。
// 第二类
// 2345671这种,也就 "是nums[start] < nums[mid]"。此例子中就是2 < 5 ;
// 这种情况下,前半部分有序。因此如果nums[start] <=target<nums [mid],则在前半部分找,否则去后半部分找。
// 第三类
// 6712345这种,也就是nums[start] > nums[mid]。此例子中就是6 > 2 ;
// 这种情况下,后半部分有序。因此如果nums [mid] <target<=nums[end] 。则在后半部分找,否则去前半部分找。
class Solution {
public:
bool search(vector<int>& nums, int target) {
int len = nums.size();
int left = 0, right = len - 1;
while (left < right) {
int mid = left + (right - left) / 2; // mid偏向与左边, 使用left=mid+1,right=mid
// 判断是否找到
if (nums[mid] == target) {
return true;
}
// 是否为第一类情况
if (nums[left] == nums[mid]) {
++left;
continue;
}
// 前半部分有序 (注:只有在有序部分中判断target,才判断出target是在前半部分还是在后半部分!)
if (nums[left] < nums[mid]) {
if (target >= nums[left] && target < nums[mid]) {
right = mid;
}
else {
left = mid + 1;
}
}
// 后半部分有序
else {
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1;
}
else {
right = mid;
}
}
}
return nums[left] == target ? true : false; // 存在因 left==right 而退出循环,从而没有判断nums[mid] == target的情况
}
};