好的,我们这次来详细讲解 LeetCode 第 33 题:搜索旋转排序数组(Search in Rotated Sorted Array)。
1. 题目描述
题意:
一个原本按升序排列的整数数组 nums,在某个未知的下标 k(0 <= k < nums.length)处进行了旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]。
例如:[0,1,2,4,5,6,7] 在下标 3 处旋转 → [4,5,6,7,0,1,2]。
给你一个目标值 target,如果 nums 中存在这个目标值,则返回它的下标,否则返回 -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
2. 题目理解与思路分析
2.1 数组特点
旋转排序数组的特点是:
- 被分成了两个有序部分(左半段和右半段),每个部分内部是升序的。
- 左半段的所有元素 ≥ 右半段的所有元素(因为原数组严格升序,旋转只是把前面一段搬到后面)。
例如:[4,5,6,7,0,1,2]
- 左半段:
[4,5,6,7](≥ 右半段所有值) - 右半段:
[0,1,2]
2.2 要求 O(log n) 时间
O(log n) 提示我们要用二分查找。
但数组不是全局有序的,直接对整个数组二分会出错,因为 mid 可能落在左半段或右半段,并且 target 可能位于任意一半。
3. 核心思路:二分查找的改造
3.1 如何判断哪部分有序
选取中点 mid = (left + right) // 2,比较 nums[mid] 与 nums[left] 或 nums[right] 可判断 mid 落在哪一半。
常用方法:比较 nums[mid] 与 nums[left]:
- 如果
nums[mid] >= nums[left],说明mid在左半段(因为左半段整体 ≥ 右半段第一个元素)。 - 否则,
mid在右半段。
3.2 判断 target 在哪一半
知道 mid 在左半段还是右半段后,就可以判断 target 是否在有序的那一半中:
-
如果
mid在左半段(即nums[left] <= nums[mid]):- 左半段有序范围是
[left, mid]。 - 如果
target在[nums[left], nums[mid]]之间,则target在左半段,应让right = mid - 1。 - 否则
target在右半段,left = mid + 1。
- 左半段有序范围是
-
如果
mid在右半段(即nums[left] > nums[mid]):- 右半段有序范围是
[mid, right]。 - 如果
target在[nums[mid], nums[right]]之间,则target在右半段,应让left = mid + 1。 - 否则
target在左半段,right = mid - 1。
- 右半段有序范围是
4. 详细步骤演示
以 nums = [4,5,6,7,0,1,2], target = 0 为例:
初始:left = 0, right = 6
Step 1:
mid = 3,nums[3] = 7,nums[left] = 4→7 >= 4→mid在左半段。- 左半段
[4,5,6,7]有序。 target = 0不在[4, 7]之间 → 去右半段找:left = mid + 1 = 4。
Step 2:left = 4, right = 6
mid = 5,nums[5] = 1,nums[left] = 0→1 >= 0? 这里注意:现在left=4,nums[left]=0,nums[mid]=1,1 >= 0成立,所以mid在左半段? 等等,这里要小心,我们应统一用nums[left]与nums[mid]比较来判断左右半段,但此时整个区间[4,6]其实是右半段整体,所以nums[left]=0,nums[mid]=1都小于nums[0]=4,所以它们都在右半段。
更稳妥的方法:比较nums[mid]与nums[left]时,left是当前区间左边界,但“左半段”“右半段”是相对于整个数组的旋转点来说的,其实我们只需判断:nums[mid] >= nums[left]说明[left, mid]有序;否则[mid, right]有序。
这里nums[5]=1,nums[4]=0→1 >= 0成立,所以[4,5]有序(确实,[0,1]升序)。- 有序区间
[4,5]的值范围[0,1],target=0在[0,1]之间吗?是的,在[nums[left]=0, nums[mid]=1]之间。 - 所以应向左半部分找:
right = mid - 1 = 4。
Step 3:left = 4, right = 4
mid = 4,nums[4] = 0,等于target,返回4。
5. 边界情况与代码实现
注意当 left == right 时的处理,以及当 nums[mid] == target 时直接返回。
def search(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
# 判断 [left, mid] 是否有序
if nums[left] <= nums[mid]:
# 左半部分有序
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else:
# 右半部分有序 [mid, right]
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1
6. 复杂度分析
- 时间复杂度:O(log n),每次将搜索区间减半。
- 空间复杂度:O(1),只用了几个变量。
7. 关键点总结
- 旋转排序数组二分时,先通过
nums[left]与nums[mid]比较判断哪一半有序。 - 判断
target是否在有序的那一半区间内,从而决定搜索方向。 - 等于
target时提前返回。 - 处理边界如
left == right和空输入。
这样,即使数组被旋转,我们也能在 O(log n) 时间内找到目标值。