【力扣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

相关推荐
大白话_NOI8 分钟前
【洛谷 P2249】查找(深基 13. 例 1)+ 详细分析
c++·算法
吠品9 分钟前
C++实现m行n列带边框的长方形输出
算法
智者知已应修善业17 分钟前
【51单片机2个外部中断显示中断历时,初始化8左移3位共阳数码管】2024-6-6
c++·经验分享·笔记·算法·51单片机
西安邮电大学1 小时前
分治算法详细讲解
java·后端·其他·算法·面试
code bean1 小时前
平衡相关性与多样性:推荐系统中的永恒博弈与 MMR 算法详解
算法
青梅橘子皮1 小时前
Linux---进程控制(2)(进程程序替换)
linux·c++·算法
Shan12051 小时前
经典问题——验证栈序列
数据结构·算法
2501_906565121 小时前
勾股定理证明
算法
Shan12052 小时前
无向图的Hierholzer算法流程(二)
算法
gihigo19982 小时前
基于蒙特卡洛的异常值剔除(RANSAC + MC置信区间)—MATLAB实现
开发语言·算法·matlab