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

一、题目概述

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

相关推荐
轻颂呀7 小时前
约瑟夫环问题
算法
凤凰院凶涛QAQ8 小时前
《Java版数据结构 & 集合类剖析》栈与队列:“push/pop 是栈的灵魂,offer/poll 是队列的骨架——四组 API,两种人生”
java·开发语言·数据结构
科技大视界8 小时前
投资AI项目,传统尽调不够用了——李章虎律师拆解算法、数据、算力三大雷区
人工智能·算法·数据挖掘
郝学胜-神的一滴9 小时前
算法实战:最小k个数——大顶堆的优雅解法
开发语言·数据结构·c++·python·程序人生·算法·排序算法
Irissgwe9 小时前
算法滑动窗口
数据结构·算法
怪兽学LLM9 小时前
LeetCode 105. 从前序与中序遍历序列构造二叉树:分治递归思路详解
算法·leetcode·职场和发展
可编程芯片开发9 小时前
基于PI控制的三相整流器控制系统的simulink建模与仿真,包含超级电容充电和电机
算法
酿情师9 小时前
区块链共识算法深度拆解:PoW、PoS、PBFT、Raft 原理解析
算法·区块链·共识算法
hansang_IR9 小时前
【记录】「SCOI2016」三道模拟赛/26.7.12
c++·算法
退休倒计时10 小时前
【每日一题】LeetCode 39. 组合总和 TypeScript
算法·leetcode·typescript