33. 搜索旋转排序数组

难度:中等 | 主题:旋转数组二分——先找有序半,再判断 target 在里面吗

题目

整数数组 nums 按升序排列,数组中的值互不相同。在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了旋转。给你旋转后的数组 nums 和一个整数 target,如果 nums 中存在这个目标值 target,则返回它的下标,否则返回 -1。要求 O(log n) 时间复杂度。

示例

 
输入:nums = [4,5,6,7,0,1,2], target = 0
 
输出:4
 

思路

先讲个故事:在被撕碎的书页里找字

你有一本字典,被人从中间撕开后把后半部分叠到了前面。现在你看到的页码是 [7,8,9,10,1,2,3,4]。你要找”第 3 页”在哪。

你会发现:撕开后至少有一半还是有序的。如果中间页码比左边大,说明左半是连续的;否则右半是连续的。只要知道哪半有序,就能判断 target 在不在那半里。


引导式推导:一次二分判断哪半有序

旋转数组任意取中点,至少一半有序。先判断哪半有序,再判断 target 是否在有序半内,从而决定搜索方向。


graph TD

    A["left=0, right=n-1"] --> B["mid = (l+r)//2"]

    B --> C{"nums[mid]==target?"}

    C -->|Yes| D["return mid"]

    C -->|No| E{"nums[l] <= nums[mid]?"}

    E -->|Yes| F["左半有序"]

    E -->|No| G["右半有序"]

    F --> H{"target在左半?"}

    G --> I{"target在右半?"}

    H -->|Yes| J["r = mid-1"]

    H -->|No| K["l = mid+1"]

    I -->|Yes| L["l = mid+1"]

    I -->|No| M["r = mid-1"]

    J --> N{"l <= r?"}

    K --> N

    L --> N

    M --> N

    N -->|Yes| B

    N -->|No| O["return -1"]

条件含义
nums[l] <= nums[mid]左半有序
nums[l] > nums[mid]右半有序
解法时间
------------
二分O(log n)

代码

 
class Solution:
 
    def search(self, nums: list[int], target: int) -> int:
 
        l, r = 0, len(nums) - 1
 
        while l <= r:
 
            mid = (l + r) // 2
 
            if nums[mid] == target:
 
                return mid
 
            if nums[l] <= nums[mid]:
 
                if nums[l] <= target < nums[mid]:
 
                    r = mid - 1
 
                else:
 
                    l = mid + 1
 
            else:
 
                if nums[mid] < target <= nums[r]:
 
                    l = mid + 1
 
                else:
 
                    r = mid - 1
 
        return -1
 

复杂度

指标解释
时间O(log n)二分查找
空间O(1)几个指针变量

实战考量

频率分析

出现在:二分进阶高频题。考察能否在「部分有序」数组中用二分。重点掌握,核心是「每次二分至少有一半有序」这个洞察。与 153 题组成旋转数组系列。

延伸思考

Q:如果数组有重复元素呢?(81 题)

A:当 nums[l] == nums[mid] == nums[r] 时无法判断哪边有序,只能 l += 1; r -= 1 缩小区间,最坏退化为 O(n)。

Q:为什么 nums[l] <= nums[mid]<= 而不是 <

A:区间只剩两个元素时 l == mid,左半只有 mid 自己,天然有序。

Q:旋转数组找最小值怎么二分?(153 题)

A:跟 nums[right] 比,nums[mid] < nums[right]right = mid(mid 可能是答案)。

易错点

  • nums[l] <= nums[mid] 的等号不能丢

  • 区间判断 mid 侧用 <(mid 已排除),边界侧用 <=

  • 循环条件 l <= r(不是 <


生活类比

搜索旋转数组 → 在被撕碎的书页里找字

字典被撕开后重叠,你翻到某一页发现页码是 9。你立刻知道:9 比前面的 4 大,说明左半(49)没被撕过,是连续的。你要找的”第 3 页”不在 49 里,那就去右半找。每次翻一页都能排除一半。


相关题目

题目关系
153寻找旋转数组最小值旋转数组基础(找最小值)
704二分查找二分基础(标准有序数组)

→ 返回题单:LeetCode学习路线图 > 八、二分查找

速记卡(面试闪卡)

Q1:一句话讲清「33. 搜索旋转排序数组」到底是什么?

A:旋转后的有序数组里二分查找,每次二分至少有一半有序,据此判方向。

Q2:一、题目与旋转数组 —— 怎么理解?

A:像被撕碎叠错的字典,要找某页在哪但要求 O(log n)。数组升序后在某点旋转,返回 target 下标或 -1(Rotated Sorted Array)。

Q3:二、先判有序半思路 —— 怎么理解?

A:像翻到一页发现页码 9 比前页大,说明左半没被撕过。中点必有一半有序,先判哪半有序再定 target 在不在(Find Sorted Half)。

Q4:三、二分收缩方向 —— 怎么理解?

A:像确认左半有序后,看 target 落不落在这段区间决定缩左还是缩右。用 <= 判左半、区间判断 mid 侧用 <(Binary Shrink)。

Q5:四、复杂度与易错点 —— 怎么理解?

A:像每次排除一半,时间 O(log n) 空间 O(1)。易错在 nums[l]<=nums[mid] 等号不能丢、循环 l<=r(Time/Space Complexity)。

Q6:核心速记主线有哪些?

  • 旋转数组每次二分至少有一半有序

  • 先判 nums[l]<=nums[mid] 哪半有序

  • 据 target 是否在有序半决定收缩方向

  • 时间 O(log n) 空间 O(1),等号别丢

口诀

A:旋转也二分,

半边总有序;

先判哪边顺,

方向就清晰。

相关链接