LeetCode 33. 搜索旋转排序数组
问题描述
给定一个在某个下标旋转过的升序排列整数数组 nums
(无重复元素)和一个目标值 target
,如果 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
算法思路
二分查找
变体:
- 核心思想:
- 旋转数组被分为两个有序部分
- 左部分>右部分
- 每次二分后,总有一半是完全有序的
- 判断逻辑:
- 若
nums[left] <= nums[mid]
,则左半部分有序 - 否则右半部分有序
- 若
- 目标值定位:
- 在有序部分内:正常二分搜索
- 不在有序部分:搜索另一部分
代码实现
class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防止溢出,mid=left
// 找到目标值直接返回
if (nums[mid] == target) {
return mid;
}
// 左半部分有序 [left, mid]
if (nums[left] <= nums[mid]) {
// 目标值在左半部分有序区间内
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1; // 在左半部分继续搜索
} else {
left = mid + 1; // 目标值在右半部分(无序)
}
}
// 右半部分有序 [mid, right]
else {
// 目标值在右半部分有序区间内
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1; // 在右半部分继续搜索
} else {
right = mid - 1; // 目标值在左半部分(无序)
}
}
}
return -1; // 未找到目标值
}
}
算法分析
- 时间复杂度:O(log n)
- 每次循环将搜索范围减半
- 空间复杂度:O(1)
- 仅使用常数空间
算法过程
nums = [4,5,6,7,0,1,2]
, target = 0
:
-
初始状态:
left=0
,right=6
mid=3
→nums[3]=7
≠ 0- 左半部分有序(
4<=7
) - 目标值0不在
[4,7]
区间 →left=4
-
第二轮:
left=4
,right=6
mid=5
→nums[5]=1
≠ 0- 左半部分有序(
0<=1
) - 目标值0在
[0,1]
区间 →right=4
-
第三轮:
left=4
,right=4
mid=4
→nums[4]=0
= target → 返回4
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例(目标在旋转点右侧)
int[] nums1 = {4,5,6,7,0,1,2};
int target1 = 0;
System.out.println("Test 1: " + solution.search(nums1, target1)); // 4
// 测试用例2:目标不存在
int[] nums2 = {4,5,6,7,0,1,2};
int target2 = 3;
System.out.println("Test 2: " + solution.search(nums2, target2)); // -1
// 测试用例3:目标在旋转点左侧
int[] nums3 = {6,7,0,1,2,4,5};
int target3 = 7;
System.out.println("Test 3: " + solution.search(nums3, target3)); // 1
// 测试用例4:无旋转情况
int[] nums4 = {1,2,3,4,5,6,7};
int target4 = 4;
System.out.println("Test 4: " + solution.search(nums4, target4)); // 3
// 测试用例5:单元素数组
int[] nums5 = {1};
int target5 = 1;
System.out.println("Test 5: " + solution.search(nums5, target5)); // 0
// 测试用例6:目标为第一个元素
int[] nums6 = {5,1,3};
int target6 = 5;
System.out.println("Test 6: " + solution.search(nums6, target6)); // 0
}
关键点
-
有序部分判定:
nums[left] <= nums[mid]
→ 左半有序nums[left] > nums[mid]
→ 右半有序
-
目标值定位:
- 在有序部分:正常二分搜索
- 在无序部分:继续二分查找
-
边界条件:
- 等号处理:
nums[left] <= nums[mid]
(包含单元素情况) - 区间开闭:左闭右开
[left, mid)
和(mid, right]
- 等号处理:
常见问题
-
为什么用
<=
判断左半有序?- 当
left==mid
时(单元素),4<=4
成立,确保正确处理单元素情况
- 当
-
如何处理旋转点在数组开头的情况?
- 此时整个数组有序,算法退化为标准二分查找
-
为什么目标值判断用开区间?
- 左半部分:
[left, mid)
(因为mid
已检查过) - 右半部分:
(mid, right]
(同上)
- 左半部分:
-
算法是否依赖无重复元素?
- 是,有重复元素时无法保证 O(log n) 时间复杂度
LeetCode 81. 搜索旋转排序数组 II
问题描述
给定一个在某个下标旋转过的非降序排列整数数组 nums
(可能包含重复元素)和一个目标值 target
,如果 target
存在于数组中则返回 true
,否则返回 false
。
示例:
输入: nums = [2,5,6,0,0,1,2], target = 0
输出: true
输入: nums = [2,5,6,0,0,1,2], target = 3
输出: false
算法思路
二分查找
变体(处理重复元素):
- 核心:
重复元素
导致无法直接判断有序区间- 最坏情况下时间复杂度退化到 O(n)
- 处理
重复元素
:- 当
nums[left] == nums[mid] == nums[right]
时,无法判断有序区间 - 此时同时收缩左右边界(
left++
,right--
)
- 当
有序区间
判断:- 若
nums[left] <= nums[mid]
,则左半部分有序 - 否则右半部分有序
- 若
- 目标值定位:
- 在有序区间内:正常二分搜索
- 不在有序区间:搜索另一部分
代码实现
class Solution {
public boolean search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;// mid=left
// 找到目标值直接返回
if (nums[mid] == target) {
return true;
}
// 处理重复元素导致无法判断有序区间的情况,只能排除nums[left]和nums[right]
if (nums[left] == nums[mid] && nums[mid] == nums[right]) {
left++;
right--;
}
// 左半部分有序 [left, mid]
else if (nums[left] <= nums[mid]) {
// 目标值在左半有序区间内
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
// 右半部分有序 [mid, right]
else {
// 目标值在右半有序区间内
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return false;
}
}
算法分析
- 时间复杂度:
- 平均情况:O(log n)
- 最坏情况(全相同元素):O(n)
- 空间复杂度:O(1)
- 仅使用常数空间
算法过程
nums = [1,0,1,1,1]
, target = 0
:
- 初始状态:
left=0
,right=4
mid=2
→nums[2]=1
≠ 0- 三值相等(
1==1==1
)→left=1
,right=3
- 第二轮:
left=1
,right=3
mid=2
→nums[2]=1
≠ 0- 左半有序(
0<=1
):- 目标0在
[0,1)
内 →right=1
- 目标0在
- 第三轮:
left=1
,right=1
mid=1
→nums[1]=0
= target → 返回true
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:目标在旋转点右侧
int[] nums1 = {2,5,6,0,0,1,2};
int target1 = 0;
System.out.println("Test 1: " + solution.search(nums1, target1)); // true
// 测试用例2:目标不存在
int[] nums2 = {2,5,6,0,0,1,2};
int target2 = 3;
System.out.println("Test 2: " + solution.search(nums2, target2)); // false
// 测试用例3:全相同元素(存在目标)
int[] nums3 = {1,1,1,1,1,1,1};
int target3 = 1;
System.out.println("Test 3: " + solution.search(nums3, target3)); // true
// 测试用例4:全相同元素(不存在目标)
int[] nums4 = {2,2,2,2,2};
int target4 = 3;
System.out.println("Test 4: " + solution.search(nums4, target4)); // false
// 测试用例5:目标在旋转点
int[] nums5 = {3,1,1,1,1};
int target5 = 3;
System.out.println("Test 5: " + solution.search(nums5, target5)); // true
// 测试用例6:重复元素干扰判断
int[] nums6 = {1,0,1,1,1};
int target6 = 0;
System.out.println("Test 6: " + solution.search(nums6, target6)); // true
}
关键点
-
重复元素处理:
if (nums[left] == nums[mid] && nums[mid] == nums[right]) { left++; right--; }
- 核心解决重复元素导致的无法判断有序区间问题
- 通过边界收缩逐步减少干扰
-
有序区间判定:
nums[left] <= nums[mid]
→ 左半有序- 否则右半有序(即使包含重复元素)
-
目标值定位:
- 在有序区间内:按正常二分搜索
- 不在有序区间:搜索另一部分
常见问题
-
为什么最坏情况是 O(n)?
- 当全数组元素相同时,每次只能收缩一个边界(如
[1,1,1,1]
中查找0
)
- 当全数组元素相同时,每次只能收缩一个边界(如
-
如何处理
target
在旋转点?- 算法能正确处理(如测试用例5),因为旋转点会被包含在有序区间判断中
-
为什么三值相等时要同时收缩?
- 因为无法确定有序区间在左还是右,但可以确定这些边界值不是目标(
mid
已检查过)
- 因为无法确定有序区间在左还是右,但可以确定这些边界值不是目标(
-
与33题(无重复元素)的主要区别?
- 增加了三值相等的特殊处理
- 有序区间判断使用
<=
而非<
- 返回值改为布尔型