从零开始刷算法——二分-搜索旋转排序数组

一、题目概述

给你一个旋转排序数组 ,其中没有重复元素,让你在其中查找指定元素 target

例如:

cpp 复制代码
nums = [4,5,6,7,0,1,2]  
target = 0 → 输出 4  
target = 3 → 输出 -1

旋转数组的结构是这样的:

cpp 复制代码
原有序数组:0 1 2 4 5 6 7
旋转 k 次:   4 5 6 7 0 1 2
                ↑       ↑
           左段仍有序   右段有序

二、我们采用的策略:两次二分

本题最佳方案之一:

✔ 第一次二分:找到旋转点(最小值的位置)

也就是找到整个数组中的 最小值的下标

例如:

cpp 复制代码
[4,5,6,7,0,1,2]  
最小值在 4 号位

第二次二分:选择正确的区间做普通的二分查找

因为旋转后的数组其实是两段独立的有序数组

cpp 复制代码
[4 5 6 7] | [0 1 2]

找到最小值的下标 i,就能得到两个有序区间:

  • 第一区:[0 ... i-1]

  • 第二区:[i ... n-1]

然后根据 target 和 nums.back() 的关系,判断 target 落在哪个有序区间,再进行一次普通二分法。


三、第一次二分:寻找最小值(旋转点)

代码如下:

cpp 复制代码
int findMin(vector<int>& nums) {
    int left = 0;
    int right = nums.size() - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > nums.back()) {
            left = mid + 1;
        }
        else {
            right = mid - 1;
        }
    }
    return left;
}

为什么比较 nums[mid] > nums.back()

因为旋转数组有一个性质:

  • 比末尾大的数字一定在左侧有序段(红区)

  • 比末尾小的数字一定在右侧有序段(蓝区,也就是最小值所在段)

我们用"红蓝染色法"理解:

区域 特点 与 nums.back() 的关系
红区(左段) 值更大 nums[mid] > nums.back()
蓝区(右段) 值更小(包括最小值) nums[mid] <= nums.back()

目标:

找到 蓝色区域(右段)中的第一个数,它就是最小值。

所以 right = mid - 1 是为了锁定蓝区的第一个元素


四、第二次二分:普通二分查找(带红蓝染色法)

为了在有序区间查找 target,我们写了一个精确的 lower_bound:

cpp 复制代码
int lower_bound(vector<int>& nums, int left, int right, int target) {
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if(nums[mid] < target) { // 左边红区
            left = mid + 1;
        }
        else {                  // 右边蓝区
            right = mid - 1;
        }
    }
    return nums[left] == target? left : -1; 
}

红区:小于 target

蓝区:大于等于 target

最终 left 会来到 蓝区的第一个位置

如果该位置恰好等于 target,则返回下标,否则返回 -1。


五、主函数逻辑:根据 target 落在哪一段

cpp 复制代码
int search(vector<int>& nums, int target) {
    int i = findMin(nums);
    // 第一段
    if (target > nums.back()){
        return lower_bound(nums, 0, i - 1, target);
    }
    else return lower_bound(nums, i, nums.size() - 1, target);
}

逻辑非常清晰:

  • 若 target > nums.back()

    → target 必在左侧红区 [0 ... i-1]

  • 否则

    → target 在右侧蓝区 [i ... n-1]

这是由旋转数组的结构决定的。

六、完整代码(推荐写法)

cpp 复制代码
class Solution {
    int findMin(vector<int>& nums) {
        int left = 0;
        int right = nums.size() - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > nums.back()) {
                left = mid + 1;
            }
            else {
                right = mid - 1;
            }
        }
        return left;
    }
    int lower_bound(vector<int>& nums, int left, int right, int target) {
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if(nums[mid] < target) { 
                left = mid + 1;
            }
            else {
                right = mid - 1;
            }
        }
        return nums[left] == target? left : -1; 
    }
public:
    int search(vector<int>& nums, int target) {
        int i = findMin(nums);
        if (target > nums.back()){
            return lower_bound(nums, 0, i - 1, target);
        }
        else return lower_bound(nums, i, nums.size() - 1, target);
    }
};

七、这种两次二分法的优势

① 逻辑清晰,结构稳定

不会出现常见的:

  • 左右边界写反

  • mid 的判断错乱

  • 死循环 / 越界等问题

② 利用红蓝染色法,可严格证明每一步正确

每个区间都有清晰的数学意义。

③ 时间复杂度依旧是 O(log n)

两次二分仍然是 O(log n)。

④ 适合拓展到更多题目

例如 154, 153, 81 等旋转数组题几乎可以模板化处理。


总结

本题的精髓不是"二分",而是结构化思维------把问题拆成两个独立的二分

  1. 找旋转点(找蓝区第一个数)

  2. 在对应有序段做普通二分(找蓝区第一个等于 target 的数)

用红蓝染色法可以让你彻底理解"为什么 mid > nums.back() 就是红区"。

相关推荐
Dev7z2 小时前
基于MATLAB数学形态学的边缘检测算法仿真实现
算法·计算机视觉·matlab
风筝在晴天搁浅9 小时前
代码随想录 718.最长重复子数组
算法
kyle~9 小时前
算法---回溯算法
算法
star _chen9 小时前
C++实现完美洗牌算法
开发语言·c++·算法
hzxxxxxxx9 小时前
1234567
算法
Sylvia-girl10 小时前
数据结构之复杂度
数据结构·算法
CQ_YM10 小时前
数据结构之队列
c语言·数据结构·算法·
VekiSon10 小时前
数据结构与算法——树和哈希表
数据结构·算法
xu_yule11 小时前
数据结构与算法(1)(第一章复杂度知识点)(大O渐进表示法)
数据结构
大江东去浪淘尽千古风流人物12 小时前
【DSP】向量化操作的误差来源分析及其经典解决方案
linux·运维·人工智能·算法·vr·dsp开发·mr