你在处理一个被旋转的股票价格序列:
[10, 15, 20, 0, 5]
,如何快速找到特定价格?传统二分查找失效时,旋转数组搜索算法正是解决这类问题的关键!
一、问题本质:旋转数组的奥秘
旋转数组指有序数组在某点旋转后得到的新数组,例如:
javascript
原数组: [0, 1, 2, 4, 5, 6, 7]
旋转后: [4, 5, 6, 7, 0, 1, 2] // 在索引3处旋转
核心挑战:
- 数组局部有序,全局无序
- 时间复杂度必须优于 O(n)
- 需处理重复值和边界情况
二、暴力解法 vs 二分优化
❌ 暴力线性搜索 (O(n))
javascript
function searchRotatedNaive(nums, target) {
for (let i = 0; i < nums.length; i++) {
if (nums[i] === target) return i;
}
return -1;
}
// 示例:searchRotatedNaive([4,5,6,7,0,1,2], 0) → 4
缺陷:百万级数据时性能雪崩
✅ 二分查找优化 (O(log n))
核心思想 :利用局部有序性分段二分
javascript
function searchRotated(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] === target) return mid; // 直接命中
// 左半段有序
if (nums[left] <= nums[mid]) {
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1; // 目标在有序左半段
} else {
left = mid + 1; // 目标在无序右半段
}
}
// 右半段有序
else {
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1; // 目标在有序右半段
} else {
right = mid - 1; // 目标在无序左半段
}
}
}
return -1;
}
执行示例 :搜索 [4,5,6,7,0,1,2]
中的 0
markdown
1. 首次: left=0, right=6, mid=3 → nums[3]=7 ≠ 0
- 左段[4,5,6,7]有序 → 0不在[4,7]区间 → 转向右段
2. 二次: left=4, right=6, mid=5 → nums[5]=1 ≠ 0
- 右段[0,1,2]有序 → 0在(1,2]区间 → left=4
3. 三次: left=4, right=4 → nums[4]=0 命中!
三、算法四步拆解
1. 确定有序区间
javascript
// 判断左侧有序的条件
const isLeftSorted = nums[left] <= nums[mid];
原理:旋转点左侧所有元素 ≥ 数组首元素
2. 目标区间定位
javascript
if (isLeftSorted) {
// 检查目标是否在有序区间内
const inLeftRange = nums[left] <= target && target < nums[mid];
}
关键点:利用有序区间的上下界判断目标位置
3. 动态调整边界
graph LR
A[判断有序区间] --> B{目标在有序区间?}
B -->|是| C[收缩到有序区间]
B -->|否| D[转向另一区间]
4. 重复值处理
当 nums[left] === nums[mid]
时:
javascript
// 处理左边界重复
if (nums[left] === nums[mid]) {
left++; // 跳过重复干扰项
continue;
}
应用场景 :如数组 [1,1,1,1,0,1]
四、性能对比与选型
方案 | 时间复杂度 | 空间复杂度 | 适用场景 |
---|---|---|---|
暴力搜索 | O(n) | O(1) | 小型数据(n<100) |
二分查找优化 | O(log n) | O(1) | 中大型数据 |
预存索引 | O(1) | O(n) | 多次查询的静态数据 |
五、举一反三:变种问题实战
场景1:查找旋转点(数组最小值)
javascript
function findPivot(nums) {
let left = 0, right = nums.length - 1;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] > nums[right]) {
left = mid + 1;
} else {
right = mid;
}
}
return left; // 返回旋转点索引
}
// 示例:findPivot([4,5,6,7,0,1,2]) → 4
场景2:含重复元素的旋转数组
javascript
function searchWithDup(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
const 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]) {
// ... 同标准解法
}
}
return false;
}
场景3:时间序列数据查询
javascript
// 应用案例:查询旋转后的股票价格时间序列
const stockPrices = [/* 时间排序的价格数组 */];
const rotatedPrices = rotate(stockPrices, 1000); // 模拟数据旋转
function queryStockPrice(price) {
return searchRotated(rotatedPrices, price) !== -1;
}
六、工程实践技巧
- 防御性编程
javascript
// 处理空数组和非法输入
if (!nums || nums.length === 0) return -1;
if (typeof target !== 'number') throw new Error('Invalid target');
- 边界加速
javascript
// 首尾直接命中可提前返回
if (nums[0] === target) return 0;
if (nums[nums.length - 1] === target) return nums.length - 1;
- 循环展开优化
javascript
while (right - left > 3) {
// ...二分逻辑
}
// 小范围直接线性搜索
for (let i = left; i <= right; i++) {
if (nums[i] === target) return i;
}
七、为什么不是哈希表?
尽管哈希表能达到 O(1) 时间复杂度:
javascript
const map = new Map();
nums.forEach((num, idx) => map.set(num, idx));
return map.get(target) ?? -1;
致命缺陷:
- 空间复杂度 O(n) 不适合大数据
- 无法利用旋转数组的局部有序特性
- 实际问题常要求空间复杂度 O(1)
在内存受限的嵌入式前端系统(如IOT设备)中,二分法更胜一筹
小结
搜索旋转排序数组的精髓在于 "在无序中寻找有序" ------ 这恰似解决复杂问题的哲学:
- 分解问题:识别局部有序片段
- 动态调整:根据反馈切换策略
- 效率至上:用 O(log n) 碾压 O(n)