LeetCode 33. 搜索旋转排序数组 | 从半边有序性质推导二分变形解法 + 全边界复盘

一、题目与核心考点

题目要求

整数数组 nums 原本是按升序排列的,且数组中的元素互不重复。在预先未知的某个下标上进行了一次旋转,例如 [0,1,2,4,5,6,7] 在下标 3 处旋转后变为 [4,5,6,7,0,1,2]。 请你在旋转后的数组中搜索目标值 target,如果数组中存在目标值则返回它的下标,否则返回 -1。要求算法时间复杂度必须是 O (log n) 级别。

核心考点

这道题的本质是有序数组变形后的二分查找,核心考察三个能力:

  1. 抓住旋转数组的局部有序性质,将问题向普通二分靠拢
  2. 边界条件与等号的精准控制,避免临界场景出错
  3. 区间收缩逻辑的严谨性,保证 O (log n) 的时间效率

最优解时间复杂度 O (log n),空间复杂度 O (1),是原地二分的标准空间开销。


二、思路完整推导:从性质到解法

1. 旋转数组的本质结构

原本严格升序的数组,在某一点旋转一次后,会变成两段连续升序、中间一个断崖下降点 的结构。 例如 [4,5,6,7,0,1,2]

  • 前半块:[4,5,6,7] 严格升序
  • 后半块:[7,0,1,2]无序

且整个数组只有这一个下降点,旋转点的位置是完全未知的 ------ 这正是题目的考点:不能先遍历找旋转点(O (n) 不符合要求),必须直接在原数组上实现 O (log n) 查找。

2. 核心铁律:mid 切分,必有一段有序

这是解题的根本,也是所有逻辑的出发点:

任取一个中间位置 mid,将当前搜索区间切分为左右两半,左右两段必有且至少有一段是完全升序的

为什么这个性质永远成立? 因为整个数组只有一个旋转断崖,mid 这一刀最多只能跨过这一个断崖:

  • 如果 mid 在断崖左侧 → 左半段完整升序,右半段跨断崖、整体无序
  • 如果 mid 在断崖右侧 → 右半段完整升序,左半段跨断崖、整体无序
  • 如果搜索区间已经缩小到单个有序块内 → 两段都有序,不影响判断

无论哪种情况,我们永远能找到一段确定有序的区间,用它来判断目标位置,每次排除一半元素。

3. 解法逻辑:盯着有序段做二分

普通二分查找的前提是「整个区间有序」,所以直接和中点比大小就能缩小区间;但旋转数组整体无序,不能直接用中点判断左右。

我们的解法就是把参照物从「中点一个值」换成「有序段的两个端点」:

  1. 先判断左、右哪半段是有序的
  2. 利用有序段的左右端点,判断 target 是否落在这个有序区间内
  3. 在区间内就收缩到这半段,不在就去另一半
  4. 循环往复,直到命中目标或区间为空

全程不需要知道旋转点在哪,靠局部有序性就能稳定收缩区间,依然保持 O (log n) 的效率。


三、最终定稿:标准单遍二分 AC 代码

java 复制代码
class Solution {
    public int search(int[] nums, int target) {
        // 边界判空
        if (nums == null || nums.length == 0) {
            return -1;
        }

        int left = 0;
        int right = nums.length - 1;

        // 左闭右闭区间二分
        while (left <= right) {
            // 计算中点,防止大数溢出
            int mid = left + (right - left) / 2;

            // 命中目标,直接返回下标
            if (nums[mid] == target) {
                return mid;
            }

            // 判断左半段 [left, mid] 是否有序
            if (nums[left] <= nums[mid]) {
                // 左半段有序:判断 target 是否在左半区间内
                if (target >= nums[left] && target < nums[mid]) {
                    right = mid - 1; // 在左半,收缩右边界
                } else {
                    left = mid + 1;  // 不在左半,去右半找
                }
            } else {
                // 右半段 [mid, right] 有序:判断 target 是否在右半区间内
                if (target > nums[mid] && target <= nums[right]) {
                    left = mid + 1;  // 在右半,收缩左边界
                } else {
                    right = mid - 1; // 不在右半,去左半找
                }
            }
        }
        // 循环结束未找到
        return -1;
    }
}

四、逐行代码深度拆解

1. 边界与初始化

java 复制代码
if (nums == null || nums.length == 0) return -1;
int left = 0;
int right = nums.length - 1;
  • 空数组直接返回 -1,防御式编程,避免后续下标访问异常
  • 采用左闭右闭 区间 [left, right],和常规二分写法一致,最容易理解和记忆

2. 循环与中点计算

java 复制代码
while (left <= right) {
    int mid = left + (right - left) / 2;
  • 左闭右闭区间对应循环条件 left <= right:当 left == right 时,区间内仍有一个元素,需要参与判断
  • left + (right - left) / 2 等价于 (left + right) / 2,但可以避免两数之和超出 int 范围导致的整数溢出,是算法题通用标准写法

3. 中点命中直接返回

java 复制代码
if (nums[mid] == target) return mid;

二分查找的常规操作,中点刚好是目标值时直接返回下标。

4. 判断左半段是否有序(最关键的一行)

java 复制代码
if (nums[left] <= nums[mid]) {

这里必须带等号 <=,绝对不能只写 <,是新手最容易翻车的细节。

  • 当区间收缩到只剩两个元素时,leftmid 下标会重合(如 left=0, right=1mid=0),此时左半段只有单个元素,天然有序
  • 如果漏写等号,该场景会误判为「左半无序」,进入错误分支,导致目标值查找失败
  • 经典反例:[3,1] 查找 1,漏写等号会直接返回 -1

5. 左半有序时的区间判断

java 复制代码
if (target >= nums[left] && target < nums[mid]) {
    right = mid - 1;
} else {
    left = mid + 1;
}
  • 左半段是升序的,因此 target 只要落在 [nums[left], nums[mid]) 区间内,就一定在左半段
  • 区间左闭右开:因为 nums[mid] 已经单独判断过不等于 target,可以直接排除
  • 在区间内就收缩右边界,不在则说明目标在右半段,收缩左边界

6. 右半有序时的区间判断

java 复制代码
if (target > nums[mid] && target <= nums[right]) {
    left = mid + 1;
} else {
    right = mid - 1;
}
  • 左半段无序时,右半段必然有序,同理判断 target 是否在右半升序区间内
  • 区间左开右闭,同样排除已判断过的 nums[mid]

五、新手必踩坑清单

坑 1:有序性判断漏写等号

  • 现象:两元素边界场景(如 [3,1])查找失败,返回错误结果
  • 根源:未覆盖 leftmid 下标重合的临界情况
  • 原则:判断有序段必须带等号,单个元素也是有序区间

坑 2:区间端点判断漏掉等号

  • 现象:target 刚好在有序段端点时,判断错误,无法命中
  • 根源:有序区间的边界开闭对应错误,端点被排除在外
  • 原则:左端点判断带 >=,右端点判断对应带 <=,与区间开闭保持一致

坑 3:边界收缩不加减 1

  • 现象:死循环或漏掉元素
  • 根源:mid 已经判断过不等于 target,不需要再保留在区间内
  • 原则:左闭右闭体系下,收缩边界必须写 mid ± 1

坑 4:中点计算溢出

  • 现象:数组长度极大时,left + right 超出 int 范围,结果为负
  • 原则:永远使用 left + (right - left) / 2 计算中点

六、拓展:两步法(先找旋转点,再二分)

如果觉得单遍法不好理解,可以采用更直观的两步法,本质是把旋转数组拆回两个有序数组,再做普通二分,时间复杂度同样是 O (log n)。

完整代码

java 复制代码
class Solution {
    public int search(int[] nums, int target) {
        if (nums == null || nums.length == 0) return -1;
        
        // 第一步:找到旋转点(数组最小值的下标)
        int minIndex = findMinIndex(nums);
        
        // 第二步:判断 target 在哪一段有序区间,执行普通二分
        if (target >= nums[minIndex] && target <= nums[nums.length - 1]) {
            return binarySearch(nums, minIndex, nums.length - 1, target);
        } else {
            return binarySearch(nums, 0, minIndex - 1, target);
        }
    }
    
    // 普通二分查找
    private int binarySearch(int[] nums, int left, int right, int target) {
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) return mid;
            if (nums[mid] < target) left = mid + 1;
            else right = mid - 1;
        }
        return -1;
    }
    
    // 二分查找旋转点(最小值下标)
    private int findMinIndex(int[] nums) {
        int left = 0;
        int right = nums.length - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > nums[right]) {
                // 中点大于右端点,最小值在右半段
                left = mid + 1;
            } else {
                // 中点小于右端点,最小值在左半段(含mid)
                right = mid;
            }
        }
        return left;
    }
}

两种方法对比

表格

方法 优点 缺点
单遍二分法 代码简洁,一次循环完成 逻辑稍抽象,需要理解局部有序性
两步法 思路直观,贴合直觉 代码稍长,需要写两次二分逻辑

面试中单遍法是主流写法,更能体现对二分变形的理解深度。


七、面试相关

口述思路(直接背)

这道题我用二分查找来实现。旋转排序数组有一个核心性质:任意中点分割后,左右两半必有一段是完全升序的。

我维护左闭右闭的搜索区间,每次取中点,如果中点就是目标值直接返回。然后通过比较左端点和中点值,判断哪一半是有序的:如果左半段有序,就判断目标值是否在左半区间内,在就收缩右边界,不在就去右半段;右半段有序同理。每次循环排除一半元素,循环结束没找到就返回 -1。

时间复杂度 O (log n),空间复杂度 O (1)。

高频追问

  1. 为什么时间复杂度是 O (log n)? 每次循环都能排除一半的元素,搜索区间规模每次减半,和普通二分查找一致,因此时间复杂度为 O (log n)。
  2. 如果数组有重复元素怎么办? 对应 LeetCode 81 题。重复元素会导致 nums[left] == nums[mid] == nums[right] 的场景,无法判断哪段有序,此时只能将左右指针各向内移动一位,最坏情况时间复杂度退化为 O (n)。
  3. 如何找到旋转点? 旋转点就是数组的最小值位置,通过二分比较中点和右端点的大小,每次排除一半区间,最终 left 和 right 收敛的位置就是最小值下标。

八、复习速记口诀

旋转二分不用慌,半边有序是脊梁; 先判哪边升序列,端点对比缩区间; 等号细节别漏掉,溢出中点要提防。


总结

搜索旋转排序数组是二分查找的经典变形,看似复杂,实则只是在普通二分的基础上,增加了「判断有序段」的步骤。 最核心的不是记住判断条件,而是理解「局部有序」的本质性质 ------ 正是因为永远有半边是确定升序的,我们才能在不知道旋转点的前提下,依然保持对数级的查找效率。 掌握这个思路后,同类型的寻找旋转最小值、搜索旋转数组 II 等题目,都可以快速迁移解法。

相关推荐
小雪崩21 小时前
嵌入式学习 day25:哈希表及排序与查找
linux·c语言·数据结构·学习·排序算法
白露与泡影21 小时前
解密 Pi 的 Harness 工程:Agent 会话如何实现持久化与恢复
java·人工智能·算法
happyprince1 天前
01-整体观-Codex全貌解读(源码)
算法·ai编程
俊基科技1 天前
别自己写算法了,让这颗37.5毫米的模组替你“闭嘴干活”
神经网络·算法·硬件开发·ai降噪·语音模块·回音消除
_Narcissus_1 天前
动态规划初探(含完整代码实现)
c语言·数据结构·c++·算法·动态规划·背包问题·最短路径
世人万千丶1 天前
鸿蒙项目实战 - 社区活动编排板:标签云布局算法与自动换行
学习·算法·华为·harmonyos·鸿蒙
zander2581 天前
LeetCode 279. 完全平方数
算法·深度优先
典典分享指南1 天前
VS Code Git 工作树:解锁多分支并行开发新体验
算法·决策树·逻辑回归·启发式算法
_Narcissus_1 天前
枚举和模拟算法笔记
c语言·数据结构·c++·笔记·算法·模拟·枚举