33. 搜索旋转排序数组(leetcode每日一题)

33. 搜索旋转排序数组 | 难度:中等

1. 题意理解(用样例说话)

先看题目给的例子:

复制代码
nums = [4,5,6,7,0,1,2], target = 0

这个数组原本是 [0,1,2,4,5,6,7],在下标 k=4 处向左旋转后变成上面的样子。

关键观察 :旋转后的数组虽然整体不再有序,但它有一个重要性质------把数组从任意位置切成两半,至少有一半是有序的

为什么?因为旋转只是把数组"掰"了一下。拿 [4,5,6,7,0,1,2] 来说:

复制代码
        7
      6    \
    5      0
  4         1
             2
  
切一刀 →
  左半 [4,5,6] 有序 ✓    右半 [7,0,1,2] 无序 ✗
  左半 [4,5,6,7] 有序 ✓  右半 [0,1,2] 有序 ✓

这个性质就是我们用二分查找的入口。

样例推演

用表格跟踪 target = 0 的查找过程:

步骤 l r 当前窗口 m nums[m] s=nums[l] e=nums[r-1] nums[m] >= s? 在哪一半 目标区间判断 行动
1 0 7 [4,5,6,7,0,1,2] 3 7 4 2 ✅(左有序) target=0 不在 [4,7) 搜右边:l=4
2 4 7 [0,1,2] 5 1 0 2 ✅(左有序) target=0 在 [0,1) 搜左边:r=5
3 4 5 [0] 4 0 0 0 ✅(左有序) nums[m]==target 返回 4

输出:4(target 0 在原数组的位置)

如果 target 不存在,比如 target = 3

步骤 l r m nums[m] s e 判断 行动
1 0 7 3 7 4 2 左有序, 3∉[4,7) l=4
2 4 7 5 1 0 2 左有序, 3∉[0,1) l=6
3 6 7 6 2 2 2 左有序, 3∉[2,2) l=7
4 7 7 --- --- --- --- l>=r 退出 返回 -1

2. 解法思路(核心)

直觉:普通二分为什么不行?

普通二分查找依赖一个前提:数组全局有序nums[m] > target 时,我们知道答案一定在左半边。

但在旋转数组中,nums[m] > target 不能告诉我们方向,因为数组是"弯"的。

关键:利用"一半有序"

虽然全局无序,但我们能保证:以 m 为界,左边和右边至少有一半是从小到大有序的

怎么判断哪一半有序?只需要比较 nums[m] 和窗口左边界 nums[l]

  • 如果 nums[m] >= nums[l]:左半 [l, m]递增的。因为旋转点一定不在这一段。
  • 如果 nums[m] < nums[l]:说明跨越了旋转点,左半不是递增的,那右半 [m, r-1] 一定是递增的

一旦确定了哪一半有序,就做两件事:

  1. 检查 target 是否落在这个有序半区的范围内。

  2. 如果在,就在这个半区继续搜;否则去另一半。

    复制代码
            ┌─────────────────────────────────┐
            │  nums[m] >= nums[l]  ?           │
            └──────────┬──────────────────────┘
                       │
           ┌───────────┴───────────┐
           ▼                       ▼
     左半有序                  右半有序
     [l, m] 递增              [m, r-1] 递增
           │                       │
     target 在                  target 在
     [nums[l], nums[m]) ?       (nums[m], nums[r-1]] ?
           │                       │
     ┌─────┴─────┐           ┌─────┴─────┐
     ▼           ▼           ▼           ▼
    是          否          是          否

    r = m l = m+1 l = m+1 r = m
    (去左边) (去右边) (去右边) (去左边)

为什么用 >= 而不是 >

考虑只剩一个元素的情况:l == m,此时 nums[m] == nums[l]

如果写成 nums[m] > nums[l](严格大于),这个条件为 false,程序会误判为"右半有序",然后去检查 (nums[m], nums[r-1]]------但此时 m == r-1,右半区间为空,逻辑就乱了。

>= 保证单个元素也被正确处理为"左半有序"。

为什么每轮都要重新算 se

s = nums[l]e = nums[r-1] 是当前搜索窗口的左右边界值。窗口每轮都在缩小,边界值当然也跟着变。把它们放在循环体内更新是必要的。


3. 代码实现

参考你提供的代码,我们可以这样实现(逐行注释):

cpp 复制代码
class Solution {
public:
    int search(vector<int>& nums, int target) {
        // 使用半开区间 [l, r),所以 r 初始化为 nums.size()
        int l = 0, r = nums.size();
        // s 和 e:当前搜索窗口的左右边界值
        int s = nums[0], e = nums[r - 1];
        int ret = -1;

        while (l < r) {                       // 半开区间:l == r 时区间为空
            int m = (l + r) / 2;
            // 每轮更新窗口边界值------窗口缩小了,边界也跟着变
            s = nums[l];
            e = nums[r - 1];

            if (nums[m] == target) {
                ret = m;
                break;
            }
            // 情况1:m 在左半有序区(nums[m] >= 窗口左边界值)
            else if (nums[m] >= s) {
                // target 落在 [s, nums[m]) 这个有序区间内吗?
                // 注意:这里用 nums[m] > target 等价于 target < nums[m],
                // 因为前面已经排除了 nums[m] == target 的情况
                if (s <= target && nums[m] > target) {
                    r = m;       // 在左半,收缩右边界
                } else {
                    l = m + 1;   // 不在左半,去右边
                }
            }
            // 情况2:m 在右半有序区(跨越了旋转点,右半才是递增的)
            else {
                // target 落在 (nums[m], e] 这个有序区间内吗?
                if (nums[m] < target && target <= e) {
                    l = m + 1;   // 在右半,收缩左边界
                } else {
                    r = m;       // 不在右半,去左边
                }
            }
        }
        return ret;
    }
};

时间复杂度:O(log n),每次迭代将搜索范围减半。

空间复杂度:O(1),只用了常数个变量。


4. 易错点(这道题的坑真的不少)

坑1:>= 还是 > ?(判断哪半有序时)

nums[m] > nums[l] 看似也可以------只要 l != m。但当区间缩到只有一个元素(l == m)时,nums[m] > nums[l] 为 false,程序会走进 "else" 分支,认为右半有序。但此时右半是空的,后续的区间判断就会完全错乱。

正确做法始终用 nums[m] >= nums[l],让单元素区间也被正确处理。

坑2:target 区间判断的边界开闭

nums[m] >= s 分支中,我们检查 target 是否在 [s, nums[m]) 范围内------左闭右开。因为如果 target == nums[m],前面已经 return 了,所以用 < target> target 都可以。写成:

cpp 复制代码
if (s <= target && nums[m] > target)  // target ∈ [s, nums[m])

这里 s <= target<= 而不是 <,因为 target 可能正好等于窗口左边界值。写成 < 会漏掉这个情况,导致无限循环或错误结果。

在 else 分支中同理,检查的是 (nums[m], e]------左开右闭:

cpp 复制代码
if (nums[m] < target && target <= e)  // target ∈ (nums[m], e]

target <= e<= 是正确的,因为 target 可能正好等于窗口右边界值。

坑3:l = m + 1 还是 l = m

半开区间 [l, r) 下:

  • 要排除 m 去右边 → l = m + 1(因为 nums[m] 已经检查过了)
  • 要排除 m 去左边 → r = m(因为 r 是开边界,自然排除 m)

写成 l = m 会导致死循环:当 r = l + 1 时,m = ll = m 意味着 l 不移动。

坑4:初始值 r = nums.size() vs r = nums.size() - 1

你的代码用了半开区间,所以 r = nums.size(),搭配 while (l < r)

如果用闭区间写法 r = nums.size() - 1,则搭配 while (l <= r),且停止条件、边界移动方式都要相应调整。两种写法都对,但必须配套。混用是最常见的 bug 来源。

坑5:忘记每轮更新 se

有人会这样写:

cpp 复制代码
int s = nums[0], e = nums.back();  // 只在循环外初始化一次
while (l < r) {
    // s 和 e 始终是初始数组的首尾值,不会随窗口缩小而更新!
    if (nums[m] >= s) { ... }
}

这是错的。随着 lr 移动,窗口边界变了,se 必须反映当前窗口 的边界值,所以每轮都要 s = nums[l]; e = nums[r-1];


5. 相关题目

题号 题目 关联点
81 搜索旋转排序数组 II 允许重复元素,需要额外处理 nums[l] == nums[m] == nums[r-1] 的情况
153 寻找旋转排序数组中的最小值 同样利用"一半有序"性质,但目标从查找特定值变成了找最小值的拐点
154 寻找旋转排序数组中的最小值 II 153 的带重复元素版本

6. 总结

这道题的核心收获:

  1. "全局不有序 ≠ 二分不能用" 。只要每次能判断目标在哪一半,二分就成立。旋转数组的特殊之处在于虽然整体有序性被破坏,但每次切分后必有一半是有序的------这是本题能用 O(log n) 的根本原因。

  2. 边界条件的精度决定成败>= vs ><= vs <、开区间 vs 闭区间------二分查找的每一个等号都不是随意的。写完后用一个元素的数组 [1] target=1、两个元素的数组 [3,1] target=1 这类极端 case 验证一遍,大部分边界 bug 会立刻暴露。

  3. 可泛化的原则 :当需要在"部分有序"的结构中搜索时,不要试图还原整个有序结构------找到那个局部不变量(本题中是"切分后必有一半有序"),然后围绕它设计判断逻辑。

相关推荐
m0_629494731 小时前
LeetCode 热题 100-----27. 合并两个有序链表
数据结构·算法·leetcode·链表
todaycode1 小时前
Vue + CPP项目
javascript·c++·vue.js
玖釉-1 小时前
Slang 和 HLSL 的区别与用法详解:现代图形渲染中的两种 Shader 编程语言
c++·算法·图形渲染
t-think1 小时前
深入了解指针(3)
c语言·算法
GIOTTO情1 小时前
Infoseek 危机公关自动化闭环系统,实现 PR 运维工程化
人工智能·算法·机器学习
hef2881 小时前
利用C 图形界面展示MATLAB算法的高效混合编程实践
windows·算法·matlab
sali-tec1 小时前
C# 基于OpenCv的视觉工作流-章76-轮廓-段距
图像处理·人工智能·opencv·算法·计算机视觉
水木流年追梦1 小时前
大模型入门-RL基础
开发语言·python·算法·leetcode·正则表达式
TechPioneer_lp1 小时前
就业指导|中九非科班毕业,华为 OD 做 Java 后端想转 C++,能找到深度学习挂钩的岗工作吗?
java·c++·华为od·华为·就业指导·校招指导