【力扣100题】80.寻找旋转排序数组中的最小值

题目描述

已知一个长度为 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, a1, a2, ..., an-1] 旋转一次的结果为数组 a\[n-1, a0, a1, a2, ..., an-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 次得到输入数组。

提示:

  • n == nums.length
  • 1 <= n <= 5000
  • -5000 <= numsi <= 5000
  • nums 中的所有整数互不相同
  • nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转

解题思路总览

方法 核心思想 时间复杂度 空间复杂度 特点
二分查找(标准版) 比较 numsmid 与 nums.back(),判断最小值在哪个区间 O(log n) O(1) 标准解法,最常考
二分查找(变形) 比较 numsmid 与 nums0,判断最小值在哪个区间 O(log n) O(1) 另一种判断方式
遍历找最小值(不推荐) 线性扫描数组 O(n) O(1) 不满足题目要求,仅作对比

方法一:二分查找(与 nums.back() 比较)

代码实现

cpp 复制代码
class Solution {
public:
    int findMin(vector<int>& nums) {
        int l = -1, r = nums.size() - 1;
        while (l + 1 < r) {
            int m = l + (r - l) / 2;
            if (nums[m] < nums.back()) {
                r = m;
            } else {
                l = m;
            }
        }
        return nums[r];
    }
};

核心思想

利用旋转排序数组的特性:

  • 数组被分成两段有序数组,第二段的第一个元素就是最小值
  • 通过比较 numsm 与 nums.back()(末尾元素)的大小关系,判断 mid 落在哪一段
  • 如果 numsm < nums.back(),说明 mid 在第二段(最小值在 l, m 区间)
  • 否则 numsm >= nums.back(),说明 mid 在第一段(最小值在 m, r 区间)

算法流程图

复制代码
以 nums = [4,5,6,7,0,1,2] 为例:

数组结构分析:
  第一段:[4,5,6,7] 所有元素 > nums.back()=2
  第二段:[0,1,2] 所有元素 <= nums.back()=2
  最小值 = 0,在第一段和第二段的交界处

二分查找过程:
初始:l=-1, r=6
第1轮:m=(-1+6)/2=2,nums[2]=6 >= nums.back()=2,l=m=2
第2轮:m=(2+6)/2=4,nums[4]=0 < nums.back()=2,r=m=4
第3轮:m=(2+4)/2=3,nums[3]=7 >= nums.back()=2,l=m=3
第4轮:m=(3+4)/2=3,l+1=4==r,循环结束
结果:nums[r]=nums[4]=0

最小值:0

逐行解析

cpp 复制代码
int l = -1, r = nums.size() - 1;

初始化二分边界:

  • l = -1,表示"哨兵"位置,在数组最左边(实际不存在)
  • r = nums.size() - 1,指向数组最后一个元素

注意:使用 l = -1 而不是 l = 0,使得最终返回值是 numsr,而 r 最终指向最小值位置。


cpp 复制代码
while (l + 1 < r) {

循环条件:l + 1 < r,即 left 和 right 之间至少有一个元素。

当 l + 1 == r 时,l 和 r 相邻,循环结束,此时 r 指向最小值。


cpp 复制代码
int m = l + (r - l) / 2;

计算中间位置,使用防溢出写法。


cpp 复制代码
if (nums[m] < nums.back()) {
    r = m;
} else {
    l = m;
}

关键判断逻辑:

  • numsm < nums.back():说明 mid 在第二段(因为第二段所有元素都 <= nums.back()),最小值在 l, m 区间,更新 r = m
  • 否则:说明 mid 在第一段,最小值在 m, r 区间,更新 l = m

cpp 复制代码
return nums[r];

循环结束时,r 指向最小值位置,返回该值。

复杂度分析

复杂度 分析
时间 每次循环将区间缩小一半,最多 O(log n) 次
空间 O(1),只使用了常数个变量

方法二:二分查找(与 nums0 比较)

代码实现

cpp 复制代码
class Solution {
public:
    int findMin(vector<int>& nums) {
        int n = nums.size();
        int left = 0, right = n - 1;
        
        while (left < right) {
            int mid = left + (right - left) / 2;
            
            if (nums[mid] < nums[0]) {
                // mid 在第二段,最小值在 [left, mid]
                right = mid;
            } else {
                // mid 在第一段,最小值在 [mid+1, right]
                left = mid + 1;
            }
        }
        
        // 如果 left == 0,说明数组没有旋转
        if (left == 0 && nums[left] <= nums[n - 1]) {
            return nums[0];
        }
        
        // 否则返回最小值(就是 left 位置)
        return nums[left];
    }
};

核心思想

与 nums0 比较来判断 mid 落在哪一段:

  • numsmid < nums0:mid 在第二段,最小值在 left, mid
  • numsmid >= nums0:mid 在第一段,最小值在 mid+1, right

与方法一的对比

对比项 方法一(与 nums.back() 比较) 方法二(与 nums0 比较)
比较对象 numsmid vs nums.back() numsmid vs nums0
边界初始化 l = -1, r = n - 1 left = 0, right = n - 1
返回值 numsr numsleft 或 nums0
循环条件 l + 1 < r left < right

复杂度分析

复杂度 分析
时间 O(log n)
空间 O(1)

方法三:直接遍历(不推荐)

代码实现

cpp 复制代码
class Solution {
public:
    int findMin(vector<int>& nums) {
        int minVal = nums[0];
        for (int i = 1; i < nums.size(); i++) {
            minVal = min(minVal, nums[i]);
        }
        return minVal;
    }
};

复杂度分析

复杂度 分析
时间 O(n),需要遍历整个数组
空间 O(1)

不推荐原因

题目要求时间复杂度为 O(log n),遍历不满足要求。但该方法思路简单直接。


边界情况分析

情况1:数组只有一元素

复制代码
输入: nums = [1]
分析: l=-1, r=0
     循环条件 l+1=-1+1=0 == r=0?不成立,循环不执行
结果: nums[r]=nums[0]=1

情况2:数组没有旋转(旋转了 n 次)

复制代码
输入: nums = [1,2,3,4,5](旋转了5次,等于没旋转)
分析: 按旋转定义,旋转 n 次后数组不变
结果: 最小值 = 1

情况3:旋转次数为 1(最小值在位置 1)

复制代码
输入: nums = [5,1,2,3,4](旋转1次)
分析: 最小值 1 在位置 1
结果: 1

情况4:旋转次数为 n-1(最小值在位置 n-1)

复制代码
输入: nums = [2,3,4,5,1](旋转4次)
分析: 最小值 1 在位置 4
结果: 1

情况5:数组完全倒序(旋转 n-1 次)

复制代码
输入: nums = [2,1](旋转1次)
分析: 最小值 1 在位置 1
结果: 1

情况6:原数组升序,未旋转

复制代码
输入: nums = [1,2,3,4,5]
分析: 由于题目说旋转 1 到 n 次,如果旋转 5 次则等于原数组
     方法一会正确处理
结果: 1

旋转排序数组特性总结

复制代码
原数组:[0, 1, 2, 4, 5, 6, 7](n=7)
旋转 k 次后的数组:[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]

旋转点(最小值位置)分析:
  旋转 0 次:[0,1,2,4,5,6,7] 最小值在位置 0
  旋转 1 次:[7,0,1,2,4,5,6] 最小值在位置 1
  旋转 2 次:[6,7,0,1,2,4,5] 最小值在位置 2
  旋转 3 次:[5,6,7,0,1,2,4] 最小值在位置 3
  旋转 4 次:[4,5,6,7,0,1,2] 最小值在位置 4
  旋转 5 次:[2,4,5,6,7,0,1] 最小值在位置 5
  旋转 6 次:[1,2,4,5,6,7,0] 最小值在位置 6
  旋转 7 次:[0,1,2,4,5,6,7] 最小值在位置 0(等于原数组)

关键发现:旋转点就是最小元素的位置。


面试追问 FAQ

问题 回答
如何找到旋转点? 旋转点就是最小元素的位置,通过二分查找可以高效定位
为什么比较 numsmid 和 nums.back()? nums.back() 是第二段的上界。如果 numsmid < nums.back(),说明 mid 在第二段
如果数组没有旋转怎么办? 旋转 n 次等于原数组,本题算法仍然正确,会返回 nums0
二分查找的循环条件 l + 1 < r 是什么意思? 表示 left 和 right 之间至少有一个元素,循环直到它们相邻
为什么初始化 l = -1 而不是 l = 0? 这样最终返回 numsr 就是最小值,而 r 最终指向最小值位置
如何处理数组为空的情况? 题目保证 n >= 1,不需要处理空数组
与第 33 题(搜索旋转排序数组)有什么区别? 本题找最小值,33 题找 target 值。本题更简单,只需要定位最小元素

相关题目

题目 难度 核心区别
153. 寻找旋转排序数组中的最小值(本题) 中等 数组中值互不相同
154. 寻找旋转排序数组中的最小值 II 困难 数组中值可能重复
33. 搜索旋转排序数组 中等 在旋转数组中查找 target
81. 搜索旋转排序数组 II 中等 在可能有重复值的旋转数组中查找 target
35. 搜索插入位置 简单 一维数组找插入位置

总结

要点 说明
核心思想 利用旋转数组两段有序的特性,通过二分查找定位最小值
关键判断 numsmid < nums.back() 说明 mid 在第二段(最小值所在段)
边界设置 l = -1, r = n - 1,使得最终返回 numsr
循环条件 l + 1 < r,直到 l 和 r 相邻
时间复杂度 O(log n)
空间复杂度 O(1)
防溢出 使用 mid = l + (r - l) / 2

相关推荐
YuK.W6 小时前
Leetcode100: 94.二叉树中序遍历、104.二叉树最大深度、226.翻转二叉树
java·算法·leetcode·二叉树
.Hypocritical.6 小时前
数据结构笔记——链表成环/反转 + 有序二叉树(BST)构建、遍历、删除
java·数据结构
气泡音人声分离7 小时前
技术解析|均衡器(EQ)工作原理与实操指南:从频率拆分到听感优化
算法·均衡器·音频剪辑
weixin_413063217 小时前
复现 MatchED 边缘检测模型(单张图片重复8次,训练200 epoch)
python·算法·计算机视觉·边缘检测模型
2601_962440847 小时前
计算机毕业设计之jsp教室管理系统
java·开发语言·笔记·分布式·算法·课程设计·推荐算法
AI视频剪辑官7 小时前
播客切片工具选型核心评价维度
网络·人工智能·算法
复杂网络10 小时前
AI 不睡觉,但它比你更会做实验
算法
贵慜_Derek10 小时前
MAI-04|干净数据在工程上意味着什么:MAI 预训练数据治理
人工智能·算法·llm
想吃火锅100511 小时前
【leetcode】146.LRU缓存js
算法·leetcode·缓存