当前位置: 首页 > news >正文

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 的查找过程:

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

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

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

步骤lrmnums[m]se判断行动
1073742左有序, 3∉[4,7)l=4
2475102左有序, 3∉[0,1)l=6
3676222左有序, 3∉[2,2)l=7
477l>=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. 代码实现

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

classSolution{public:intsearch(vector<int>&nums,inttarget){// 使用半开区间 [l, r),所以 r 初始化为 nums.size()intl=0,r=nums.size();// s 和 e:当前搜索窗口的左右边界值ints=nums[0],e=nums[r-1];intret=-1;while(l<r){// 半开区间:l == r 时区间为空intm=(l+r)/2;// 每轮更新窗口边界值——窗口缩小了,边界也跟着变s=nums[l];e=nums[r-1];if(nums[m]==target){ret=m;break;}// 情况1:m 在左半有序区(nums[m] >= 窗口左边界值)elseif(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;// 不在右半,去左边}}}returnret;}};

时间复杂度: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都可以。写成:

if(s<=target&&nums[m]>target)// target ∈ [s, nums[m])

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

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

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()vsr = nums.size() - 1

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

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

坑5:忘记每轮更新se

有人会这样写:

ints=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寻找旋转排序数组中的最小值 II153 的带重复元素版本

6. 总结

这道题的核心收获:

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

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

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

http://www.jsqmd.com/news/868190/

相关文章:

  • 软文营销底层逻辑重构专业发稿平台成品牌流量核心抓手
  • cann-learning-hub:从零开始学昇腾的最短路径
  • 技术日报|Claude Code代码知识图谱codegraph单日揽星4294夺冠,superpowers突破20万星历史里程碑
  • 从QLineEdit到QChartView:用Qt控件组合打造一个简易数据监控仪表盘
  • 2026年5月云南基建选材指南:聚焦耐用钢筋混凝土排水管实力品牌 - 2026年企业推荐榜
  • Astral uv速度快却包管理体验差,开发者呼吁改进命令与版本约束
  • Triton + CANN GE Backend:大模型推理服务部署
  • 从手机到机器人:拆解RGB-D摄像头(如Kinect、RealSense)如何‘看见’三维世界
  • Chromium 146 编译指南 Windows篇:Git 安装与高级配置(二)
  • Antigravity IDE规则
  • NotebookLM支持实时字幕吗?不,它真正强悍的是这4种高阶语音语义重构能力
  • C51编译器浮点数支持与嵌入式优化实践
  • 如何让AI Agent安全可控地工作?Markus治理体系深度解析
  • 全网首曝:ChatGPT在金融/医疗/法律三大高敏领域生成高质量文章的合规性锚点(含GDPR+HIPAA双认证提示模板)
  • pto-isa:昇腾 Graph Compiler 的虚拟指令集
  • 评价高的四轴直驱电机厂家哪家靠谱
  • C# 基于OpenCv的视觉工作流-章76-轮廓-段距
  • 【MySQL 三大日志深度解析】:redo log、undo log、binlog 作用与两阶段提交原理
  • Django 从 0 到 1 打造完整电商平台:收货地址管理
  • Windows 11/10系统瘦身与性能优化:手把手教你用DISM禁用不常用功能
  • 得物数仓AI开发痛点多,Harness工程四层分工让研发流水线更可靠!
  • ubuntu虚拟环境中安装python包,报错
  • MPI_Win_allocate_shared介绍和使用
  • ops-softmax:Transformer 推理中的概率归一化引擎
  • 贴片晶振的广泛应用与768kHz晶振的优势
  • 阿里巴巴与厦门大学联手打造“时装变色龙“
  • OpenClaw:高效管理分布式Agent开发团队
  • Claude Code 国内替代方案:基于百炼的配置与实践
  • Newman安装之nodejs下载安装
  • ops-reduce:ReduceMax 与 ReduceMean 的并行优化