搜索旋转排序数组:二分查找的极致优化

你在处理一个被旋转的股票价格序列:[10, 15, 20, 0, 5],如何快速找到特定价格?传统二分查找失效时,旋转数组搜索算法正是解决这类问题的关键!


一、问题本质:旋转数组的奥秘

旋转数组指有序数组在某点旋转后得到的新数组,例如:

javascript 复制代码
原数组: [0, 1, 2, 4, 5, 6, 7]
旋转后: [4, 5, 6, 7, 0, 1, 2]  // 在索引3处旋转

核心挑战

  1. 数组局部有序,全局无序
  2. 时间复杂度必须优于 O(n)
  3. 需处理重复值和边界情况

二、暴力解法 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;
}

六、工程实践技巧

  1. 防御性编程
javascript 复制代码
// 处理空数组和非法输入
if (!nums || nums.length === 0) return -1;
if (typeof target !== 'number') throw new Error('Invalid target');
  1. 边界加速
javascript 复制代码
// 首尾直接命中可提前返回
if (nums[0] === target) return 0;
if (nums[nums.length - 1] === target) return nums.length - 1;
  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;

致命缺陷

  1. 空间复杂度 O(n) 不适合大数据
  2. 无法利用旋转数组的局部有序特性
  3. 实际问题常要求空间复杂度 O(1)

在内存受限的嵌入式前端系统(如IOT设备)中,二分法更胜一筹


小结

搜索旋转排序数组的精髓在于 "在无序中寻找有序" ------ 这恰似解决复杂问题的哲学:

  1. 分解问题:识别局部有序片段
  2. 动态调整:根据反馈切换策略
  3. 效率至上:用 O(log n) 碾压 O(n)
相关推荐
斯普信专业组22 分钟前
2025 最好的Coze入门到精通教程(下)
前端·javascript·ui
德育处主任1 小时前
p5.js 圆弧的用法
前端·javascript·canvas
jstart千语1 小时前
【力扣】第42题:接雨水
算法·leetcode·职场和发展
墨染点香1 小时前
LeetCode 刷题【10. 正则表达式匹配】
算法·leetcode·职场和发展
cookqq1 小时前
mongodb源代码分析createCollection命令由create.idl变成create_gen.cpp过程
数据库·算法·mongodb·nosql
clock的时钟1 小时前
数据结构-线性表顺序表示
数据结构·算法
WanderInk2 小时前
深入解析:Java Arrays.sort(intervals, Comparator.comparingInt(a -> a[0])); 一行代码的背后功力
java·后端·算法
এ᭄画画的北北2 小时前
力扣-70.爬楼梯
算法·leetcode
Arvin6272 小时前
Nginx IP授权页面实现步骤
服务器·前端·nginx
nako_sayuri2 小时前
二分查找:区间内查询数字的频率
数据结构·算法