跳转至

153. 寻找旋转排序数组中的最小值

LeetCode 1531 · 中等 · 二分查找

题

一个元素互不相同的升序数组被旋转了若干次(如 [0,1,2,4,5,6,7] 变成 [4,5,6,7,0,1,2],也可能转回原样),返回其中的最小值。要求 \(O(\log n)\)。

例:nums = [3, 4, 5, 1, 2] → 1;nums = [11, 13, 15, 17] → 11。

一句话

后段每个数都 <= nums[-1],前段每个数都 > nums[-1]——这个谓词单调,最小值就是后段的起点,一个 lower_bound 找出来。

关键技巧

二分不需要数组有序,只需要谓词单调。 旋转数组 = 大的前段 + 小的后段,后段以 nums[-1] 结尾。定义

\[ P(i) = [\,nums_i \le nums_{n-1}\,] \]

前段所有元素都大于后段所有元素,自然大于 \(nums_{n-1}\),所以前段全 F;后段升序且以 \(nums_{n-1}\) 收尾,全 T。谓词形如 F…F T…T,第一个 T 就是最小值。没旋转时整个数组都是后段,答案是下标 0,同样成立。

区间不变量:左闭右开 \([lo, hi)\),\([0, lo)\) 全 F,\([hi, n)\) 全 T。因为 \(P(n-1)\) 一定为 T,可以直接从 hi = n - 1 开始(下标 \(n-1\) 已知为 T),循环结束 lo 就是答案,且一定合法。

为什么和 nums[-1] 比而不是 nums[0]:和 nums[0] 比,没旋转的数组会全是 T(>= nums[0]),分界点跑到 \(n\),要特判。末尾元素在两种情况下都属于后段,无需特判。

解

class Solution:
    def findMin(self, nums: List[int]) -> int:
        lo, hi = 0, len(nums) - 1  # [lo, hi) 未知;下标 n-1 已知满足谓词
        while lo < hi:
            mid = (lo + hi) // 2
            if nums[mid] <= nums[-1]:
                hi = mid
            else:
                lo = mid + 1
        return nums[lo]

时间 \(O(\log n)\),空间 \(O(1)\)。

延伸

自测
s = Solution()
assert s.findMin([3, 4, 5, 1, 2]) == 1
assert s.findMin([4, 5, 6, 7, 0, 1, 2]) == 0
assert s.findMin([11, 13, 15, 17]) == 11
assert s.findMin([1]) == 1
assert s.findMin([2, 1]) == 1
base = list(range(10))
assert all(s.findMin(base[k:] + base[:k]) == 0 for k in range(10))

  1. LeetCode 153,2026-09-29 读。 ↩