给定一个由不同元素构成的旋转排序数组(原本是升序排列,但在某个未知点进行了旋转),要求快速找到目标元素的索引。如果不存在,则返回 -1。
示例 1:
- 输入:
arr = [5, 6, 7, 8, 9, 10, 1, 2, 3],key = 3 - 输出:
8
示例 2:
- 输入:
arr = [3, 5, 1, 2],key = 6 - 输出:
-1
示例 3:
- 输入:
arr = [33, 42, 72, 99],key = 42 - 输出:
1
目录
- [朴素解法:线性搜索 - O(n) 时间 & O(1) 空间](#朴素解法:线性搜索 - O(n) 时间 & O(1) 空间)
- [期望解法一:两次二分查找 - O(log n) 时间 & O(1) 空间](#期望解法一:两次二分查找 - O(log n) 时间 & O(1) 空间)
- [期望解法二:单次二分查找 - O(log n) 时间 & O(1) 空间](#期望解法二:单次二分查找 - O(log n) 时间 & O(1) 空间)
1. 朴素解法:线性搜索
最直接的方式就是遍历整个数组,逐个元素与目标值比较。找到则返回索引,否则返回 -1。
复杂度分析:
- 时间复杂度: O(n) ------ 最坏情况下需要检查所有元素。
- 空间复杂度: O(1) ------ 只用了常数个变量。
代码示例(Python):
python
def search(arr, key):
for i in range(len(arr)):
if arr[i] == key:
return i
return -1
虽然简单,但效率不高。当数组很大时,线性搜索会非常慢。接下来我们看看如何利用旋转排序数组的特性来加速。
说到旋转排序数组的二分查找,很多人卡在"如何判断哪半有序"这个点上。死磕代码不如亲眼看到指针如何移动------强烈安利一个叫图码的网站,它把60多种算法做成交互式动画,你可以自己输入测试数据,甚至上传C/C++/Java/Python代码,看着代码一行行跑出动画。这个工具专门为408考研和数据结构期末考试设计,全书级知识点+可运行代码,遇到不懂的还能7x24小时选中代码让AI解释。学算法可视化,上图码就对了。
图码-数据结构与算法交互式可视化平台
访问网站:https://totuma.cn
2. 期望解法一:两次二分查找
核心思路是:先找到数组中**最小元素(即旋转点)**的索引,这样就把原数组分成了两个有序的子数组。然后根据目标值与第一个元素的大小关系,决定在哪个子数组上进行标准的二分查找。
步骤详解:
- 找到最小元素的索引
pivot(旋转点)。 - 如果
arr[pivot] == key,直接返回pivot。 - 如果
pivot == 0,说明整个数组是未旋转的,直接对整个数组做二分查找。 - 否则,比较
key和arr[0]:- 若
key >= arr[0],则在左半部分[0, pivot-1]进行二分查找。 - 否则,在右半部分
[pivot+1, n-1]进行二分查找。
- 若
复杂度分析:
- 时间复杂度: O(log n) ------ 两次二分查找,每次都是 O(log n)。
- 空间复杂度: O(1) ------ 只用了常数个变量。
代码示例(Python):
python
def binarySearch(arr, lo, hi, x):
while lo <= hi:
mid = lo + (hi - lo) // 2
if arr[mid] == x:
return mid
if arr[mid] < x:
lo = mid + 1
else:
hi = mid - 1
return -1
def findPivot(arr, lo, hi):
while lo <= hi:
if arr[lo] <= arr[hi]:
return lo
mid = (lo + hi) // 2
if arr[mid] > arr[hi]:
lo = mid + 1
else:
hi = mid
return lo
def search(arr, key):
n = len(arr)
pivot = findPivot(arr, 0, n - 1)
if arr[pivot] == key:
return pivot
if pivot == 0:
return binarySearch(arr, 0, n - 1, key)
if arr[0] <= key:
return binarySearch(arr, 0, pivot - 1, key)
return binarySearch(arr, pivot + 1, n - 1, key)
输出: 8
3. 期望解法二:单次二分查找
更优雅的做法是直接对旋转数组进行一次修改过的二分查找 。在每一次迭代中,我们检查中间元素 arr[mid] 是否等于目标值。如果不是,我们就判断左半部分还是右半部分是有序的,然后根据目标值是否落在该有序区间内来调整搜索范围。
算法流程:
- 初始化
lo = 0,hi = n-1。 - 当
lo <= hi时:- 计算
mid = lo + (hi - lo) // 2。 - 如果
arr[mid] == key,返回mid。 - 判断左半部分是否有序:
arr[mid] >= arr[lo]。- 如果左半部分有序且
key在[arr[lo], arr[mid])范围内,则hi = mid - 1,否则lo = mid + 1。
- 如果左半部分有序且
- 否则(右半部分有序):
- 如果
key在(arr[mid], arr[hi]]范围内,则lo = mid + 1,否则hi = mid - 1。
- 如果
- 计算
- 如果循环结束未找到,返回 -1。
复杂度分析:
- 时间复杂度: O(log n) ------ 每次迭代将搜索范围缩小一半。
- 空间复杂度: O(1) ------ 只用了常数个变量。
代码示例(Python):
python
def search(arr, key):
lo, hi = 0, len(arr) - 1
while lo <= hi:
mid = lo + (hi - lo) // 2
if arr[mid] == key:
return mid
# 左半部分有序
if arr[mid] >= arr[lo]:
if key >= arr[lo] and key < arr[mid]:
hi = mid - 1
else:
lo = mid + 1
# 右半部分有序
else:
if key > arr[mid] and key <= arr[hi]:
lo = mid + 1
else:
hi = mid - 1
return -1
输出: 8
总结
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 线性搜索 | O(n) | O(1) | 简单但慢 |
| 两次二分 | O(log n) | O(1) | 先找旋转点,再二分 |
| 单次二分 | O(log n) | O(1) | 直接修改二分查找逻辑 |
推荐使用单次二分查找,因为它代码更简洁,且不需要额外寻找旋转点。
掌握旋转排序数组的搜索,不仅能应对面试题,更能加深对二分查找变体的理解。下次遇到类似问题,记得试试这个技巧哦!