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

一、题目概述

给你一个旋转排序数组 ,其中没有重复元素,让你在其中查找指定元素 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() 就是红区"。

相关推荐
做怪小疯子2 小时前
LeetCode 热题 100——哈希——最长连续序列
算法·leetcode·哈希算法
做怪小疯子3 小时前
LeetCode 热题 100——双指针——三数之和
算法·leetcode·职场和发展
高山上有一只小老虎3 小时前
等差数列前n项的和
java·算法
sin_hielo3 小时前
leetcode 2536
数据结构·算法·leetcode
flashlight_hi3 小时前
LeetCode 分类刷题:203. 移除链表元素
算法·leetcode·链表
py有趣3 小时前
LeetCode算法学习之数组中的第K个最大元素
学习·算法·leetcode
吗~喽3 小时前
【LeetCode】将 x 减到 0 的最小操作数
算法·leetcode
what_20184 小时前
list集合使用
数据结构·算法·list
hetao17338374 小时前
2025-11-13~14 hetao1733837的刷题记录
c++·算法