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

C++ 面试高频考点 力扣 153. 寻找旋转排序数组中的最小值 二分查找 题解 每日一题

文章目录

  • 题目描述
  • 为什么这道题值得你用几分钟的时间弄懂?
  • 二分的依据
  • 二分的两种不同思路
    • 右端点(nums[nums.size() - 1])
      • 代码实现
    • 左端点(nums[0])
      • 代码实现
  • 细节总结
  • 下题预告

在这里插入图片描述
在这里插入图片描述

题目描述

题目链接:
力扣153.寻找旋转排序数组中的最小值

题目描述:
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。

示例 2:
输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。

示例 3:
输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。

提示:
1.n == nums.length
2.1 <= n <= 5000
3.-5000 <= nums[i] <= 5000
4.nums 中的所有整数 互不相同
5.nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转

为什么这道题值得你用几分钟的时间弄懂?

首先这道题的细节思路差异有很多,当我们完全理清细节上的差异时,会对二分查找有更深刻的理解。其次,这道题利用“数组旋转后仍保留部分单调性”的性质来分析二段性,这个逻辑比基础二分查找稍难理解——它不是直接对“整体有序”的数组操作,而是对“局部有序、分段有序”的数组拆解,能帮你突破对二分查找的固有认知。

我不会着重讨论二分查找的通用细节(如 mid 计算方式、循环终止条件等),如果是第一次接触我的博客,建议先从这篇基础博客开始力扣 704.二分查找 基础二分查找,可以进入我的主页这段时间我做的题目都是关于二分查找的,可以从上面的那个博客往后看相信会让你对二分理解的更加深刻;如果是一直跟进的老朋友,直接往下看即可,逻辑衔接会很顺畅。

二分的依据

这道题乍看数组“忽高忽低”,似乎无法用二分,但核心关键在于:旋转前的数组是严格单调递增的,旋转后仍会保留“两段单调递增”的特性——我们可以基于这个特性找到“二段性”,进而用二分解决问题。

具体来说,旋转后的数组只会出现两种情况:

  1. 未发生有效旋转(或旋转后回到原状态):数组仍为整体单调递增,例如 [11,13,15,17]
  2. 发生有效旋转:数组被拆分为左右两段独立的单调递增子数组,且左段的所有元素都大于右段的所有元素。例如 [4,5,6,7,0,1,2] 拆分为左段 [4,5,6,7] 和右段 [0,1,2],左段最小值 4 大于右段最大值 2,如下图👇(左段 [4,5,6,7]对应AB ,右段 [0,1,2]对应CD)

在这里插入图片描述

正是这种“要么整体有序,要么两段有序且左段>右段”的特性,为二分查找提供了判断依据——我们可以通过与数组的端点值对比,确定最小值所在的区间,进而缩小范围。

二分的两种不同思路

上面我们确定了数组的二段性,接下来关键是“如何利用端点值划分区间”。这里有两种核心思路:以右端点为基准,或以左端点为基准。

右端点(nums[nums.size() - 1])

首先明确一个结论:无论数组是否旋转,右端点(D)一定属于“右段单调子数组”(若未旋转,整个数组就是右段),且右段的所有元素都小于等于右端点(因为右段单调递增),左段的所有元素都大于右端点(因为左段>右段)。
在这里插入图片描述

例如:

  • 旋转数组 [4,5,6,7,0,1,2]:右端点是 2,右段 [0,1,2] 都 ≤ 2,左段 [4,5,6,7] 都 > 2;
  • 未旋转数组 [11,13,15,17]:右端点是 17,整个数组(右段)都 ≤ 17,无左段。

基于这个结论,当我们计算中间值 nums[mid] 时,会出现两种情况:

1. nums[mid] < nums[right](中间值小于右端点)
结合下图,此时 nums[mid] 必然属于右段(因为左段元素都大于右端点,不可能小于右端点)。由于右段单调递增,最小值一定在 mid 左侧(包括 mid 本身)——因为 mid 右侧的元素都 ≥ nums[mid],不可能是最小值。

在这里插入图片描述
例如:nums = [4,5,6,7,0,1,2]right=6(值为2),若 mid=4(值为0),0 < 2,则最小值在 [0,4] 区间内(实际最小值就是 0)。

因此,我们需要舍弃右半部分,令 right = mid

2. nums[mid] >= nums[right](中间值大于等于右端点)
结合下图,此时 nums[mid] 必然属于左段(因为右段元素都 ≤ 右端点,不可能大于右端点)。由于左段单调递增且左段>右段,mid 及其左侧的元素都大于右段元素,最小值一定在 mid 右侧——mid 本身不可能是最小值。

在这里插入图片描述

例如:nums = [4,5,6,7,0,1,2]right=6(值为2),若 mid=2(值为6),6 ≥ 2,则最小值在 [3,6] 区间内(排除了左段 [4,5,6])。

因此,我们需要舍弃左半部分,令 left = mid + 1

代码实现

class Solution {
public:int findMin(vector<int>& nums) {int n = nums.size();int left = 0, right = n - 1;int x = nums[right]; // 提前存储右端点值,避免重复计算while (left < right) { // 循环终止时 left == right,即为最小值下标int mid = left + (right - left) / 2; // 避免溢出,等价于 (left+right)/2if (nums[mid] < x) {right = mid; // 舍弃右半部分,最小值在左侧} else {left = mid + 1; // 舍弃左半部分,最小值在右侧}}return nums[left]; // 此时 left == right,返回对应值}
};

左端点(nums[0])

以左端点为基准的逻辑稍复杂,先明确结论:左端点一定属于“左段单调子数组”(若未旋转,整个数组就是左段),且左段的所有元素都大于等于左端点(左段单调递增),右段的所有元素都小于左端点(左段>右段)。
在这里插入图片描述

例如:

  • 旋转数组 [4,5,6,7,0,1,2]:左端点是 4,左段 [4,5,6,7] 都 ≥ 4,右段 [0,1,2] 都 < 4;
  • 未旋转数组 [11,13,15,17]:左端点是 11,整个数组(左段)都 ≥ 11,无右段。

这里的关键问题是:未旋转数组会打破“左段>右段”的逻辑——此时数组整体递增,若仍按“左端点基准”判断,会出现错误。因此,以左端点为基准时,需要先处理“未旋转”的特殊情况。

接下来,在“已旋转”的前提下,计算中间值 nums[mid] 会出现两种情况,咱们将特殊情况放到最后说:

1. nums[0] > nums[mid](左端点大于中间值)
结合下图,此时 nums[mid] 必然属于右段(因为左段元素都 ≤ 左端点,不可能小于左端点)。由于右段单调递增,最小值一定在 mid 左侧(包括 mid 本身)——因为 mid 右侧的元素都 ≥ nums[mid]
在这里插入图片描述
例如:nums = [4,5,6,7,0,1,2]nums[0] = 4,若 mid=4(值为0),4 > 0,则最小值在 [0,4] 区间内(实际最小值就是 0)。

因此,我们需要舍弃右半部分,令 right = mid

2. nums[0] <= nums[mid](左端点小于等于中间值)
结合下图,此时 nums[mid] 必然属于左段(因为右段元素都 < 左端点,不可能大于等于左端点)。由于左段单调递增且左段>右段,mid 及其左侧的元素都大于右段元素,最小值一定在 mid 右侧——mid 本身不可能是最小值。
在这里插入图片描述

例如:nums = [4,5,6,7,0,1,2]nums[0] = 4,若 mid=2(值为6),4 ≤ 6,则最小值在 [3,6] 区间内(排除了左段 [4,5,6])。

因此,我们需要舍弃左半部分,令 left = mid + 1

nums[0] <= nums[mid]的特殊情况处理
如前所述,若数组未旋转(整体递增),例如 [11,13,15,17],此时 nums[0] = 11,无论 mid 取何值,nums[mid] 都 ≥ nums[0],会触发“left = mid + 1”的逻辑,导致 left 不断右移就(我的left要远航了…),最终错过最小值 11
在这里插入图片描述
因此,以左端点为基准时,必须先判断数组是否未旋转:若 nums[left] < nums[right](整体递增),直接返回 nums[left](即左端点,也是最小值)。

代码实现

class Solution {
public:int findMin(vector<int>& nums) {int n = nums.size();int left = 0, right = n - 1;// 特殊情况:数组未旋转(整体递增),直接返回左端点(最小值)if (nums[left] < nums[right]) {return nums[left];}int x = nums[left]; // 提前存储左端点值,避免重复计算while (left < right) { // 循环终止时 left == right,即为最小值下标int mid = left + (right - left) / 2; // 避免溢出if (x > nums[mid]) {right = mid; // 舍弃右半部分,最小值在左侧} else {left = mid + 1; // 舍弃左半部分,最小值在右侧}}return nums[left]; // 此时 left == right,返回对应值}
};

细节总结

通过上面两种思路的对比,我们可以解答两个关键细节问题:

1. 为什么右端点不用判断是否为单调递增数组,左端点却要判断?

核心原因是两种基准的“特殊情况兼容性”不同

  • 对于右端点基准:未旋转数组的“整体递增”可以被纳入“右段单调递增”的逻辑中——此时右段就是整个数组,nums[mid] < nums[right] 会始终成立,right 会不断左移,最终 left == right 指向左端点(最小值),无需额外判断;
  • 对于左端点基准:未旋转数组的“整体递增”会打破“左段>右段”的逻辑——此时无右段,nums[0] <= nums[mid] 始终成立,left 会不断右移,最终指向右端点(最大值),导致错误。因此必须提前判断“整体递增”的情况。

2. x = nums[] 有必要一定写吗?
在我尝试这道题目的时候最开始没有用一个x来单独存储最左或最右侧的点,但是也能跑过所以我想在总结这里讨论下:写不写都能跑通的原因是核心是因为数组端点值(nums[right]nums[left])在整个二分过程中始终固定不变,重复访问也不会出错,只是用 x 存储能减少数组下标重复访问的开销、让代码更直观。

虽然不是“必须”,但强烈建议写,原因有两点:
(1)提升代码可读性:用 x 代表“基准端点值”,循环内的 if (nums[mid] < x)if (nums[mid] < nums[right]) 更直观,能让读者一眼看出“当前判断是基于基准值”,降低理解成本。
(2)避免重复计算:在循环中,nums[right]nums[left] 的值不会变化(rightleft 是下标,端点值固定),提前用 x 存储后,循环内只需访问 x,无需重复访问数组下标,减少操作开销;

下题预告

如果觉得这些内容对你有帮助,不妨点个赞支持一下,再关注我的博客。后续我还会持续分享更多算法干货,跟着系列文章一步步学,你对二分查找的掌握一定会越来越扎实~

下一篇博客我们将讨论力扣中的 LCR 173. 点名(本质是“在单调递增数组中找缺失元素”,同样可以用二分查找高效解决,且会涉及新的二段性分析思路)。


文章转载自:

http://CDm7tHhZ.Lgpzq.cn
http://jYKb4Pba.Lgpzq.cn
http://zRlBQEAu.Lgpzq.cn
http://zI3hlqGx.Lgpzq.cn
http://D4bOvJFe.Lgpzq.cn
http://uTFIW5nI.Lgpzq.cn
http://DkYN65Dq.Lgpzq.cn
http://4NDpg9GH.Lgpzq.cn
http://oGnllDOy.Lgpzq.cn
http://MyIdIGVD.Lgpzq.cn
http://k7gmuA90.Lgpzq.cn
http://2Qu53UDi.Lgpzq.cn
http://cGXz5xCM.Lgpzq.cn
http://5HvlVBGP.Lgpzq.cn
http://SOoxug3F.Lgpzq.cn
http://ZuZAzUyb.Lgpzq.cn
http://iHkdjXlZ.Lgpzq.cn
http://HUvIOth7.Lgpzq.cn
http://O4CsjX7C.Lgpzq.cn
http://sNzlGL6z.Lgpzq.cn
http://gBjAbrIu.Lgpzq.cn
http://sR3WHhPD.Lgpzq.cn
http://Lfn5beDB.Lgpzq.cn
http://mpnTGcJ8.Lgpzq.cn
http://RzBbuF6N.Lgpzq.cn
http://V7Uo8IDt.Lgpzq.cn
http://pdGWrbag.Lgpzq.cn
http://px3l1vwI.Lgpzq.cn
http://ZZXzueg3.Lgpzq.cn
http://9NQo51A3.Lgpzq.cn
http://www.dtcms.com/a/366463.html

相关文章:

  • C++ opencv+gstreamer编译,C++ opencv4.5.5+gstreamer1.0 -1.24.12 编译 ,cmake 4.0.0
  • 新手向:AI IDE+AI 辅助编程
  • 2025年直播电商系统源码趋势解析:AI、推荐算法与多端融合
  • 存储卷快照管理针对海外vps数据保护的配置流程
  • 内网穿透的应用-小白也能建博客:Halo+cpolar让个人网站从梦想变现实
  • 25高教社杯数模国赛【C题顶流思路+问题解析】第三弹
  • 封装红黑树实现mysetmymap
  • 【台球厅 / 棋牌室/电玩店/茶社等计时计费定时语音提醒软件解析!】佳易王 计时计费高级版V18.3 实测:双时间显示 + 语音提醒 + 智能记事全解析
  • 【C++】16. set和map
  • mysql安装(压缩包方式8.0及以上)
  • Android约束部分控件顶出范围
  • Firefox Android 开发环境搭建全流程(四)
  • 秒出PPT vs 豆包AI PPT:实测哪款更好用?
  • Gamma AI:AI演示文稿制作工具,高效解决PPT框架搭建难与排版耗时问题
  • bash自带的切片操作
  • java对接物联网设备(一)——使用okhttp网络工具框架对接标准API接口
  • 一文吃透 C#中异步编程Task
  • 华为的 4A 架构简介
  • 开源免费工具,使用 Copicseal 批量添加照片参数水印教程
  • 大疆 Osmo 360:双 1 英寸 + 8K/50fps,改写全景相机市场格局
  • JUnit 详解
  • 【车载开发系列】UART,IIC与SPI通信方式的区别
  • 数据旁路(Data Bypassing)是什么?
  • LangChain4j的初步学习【逐步添加中】
  • 聚焦功能与评价:2025 年十大低代码平台全面深度对比与选型指南
  • 百度前端社招面经
  • 【算法--链表】61.旋转链表--通俗讲解
  • [特殊字符]为什么不建议全局安装 Claude Code?官方推荐的最佳实践与代理配置指南
  • 盲盒抽卡机小程序系统开发:以技术创新驱动娱乐体验升级
  • 《用于触觉伺服操作的高动态触觉传感:让机器人摆动锤子》论文解读