LeetCode 33:搜索旋转排序数组——Java 两阶段二分查找详解

一、题目描述

给定一个元素互不相同、原本按升序排列的整数数组 nums。数组在传入函数前,可能在某个未知下标处发生了旋转。

例如,原数组:

复制代码
[0,1,2,4,5,6,7]

在下标 3 处旋转后,可能变成:

复制代码
[4,5,6,7,0,1,2]

再给定一个目标值 target,如果它存在于数组中,则返回对应下标;否则返回 -1。题目要求时间复杂度为 O(log n)

普通升序数组可以直接二分查找,但旋转后的数组不再整体有序,因此不能直接套用标准模板。截图中的解决方案分为两步:先找到最小值下标,将数组划分成两个升序区间;再确定目标值属于哪一段,并执行普通二分查找。

二、旋转数组的结构

观察数组:

复制代码
[4,5,6,7,0,1,2]

虽然它整体不是升序,但可以按照最小值 0 的位置拆成两段:

复制代码
[4,5,6,7]  [0,1,2]

这两个子区间内部都保持升序。因此,只要先找到最小值所在下标 minIndex,就能得到两个可进行二分查找的区间:

复制代码
[0, minIndex - 1]
[minIndex, nums.length - 1]

所以本题可以拆成两个熟悉的问题:

  1. 二分查找旋转数组中的最小值;

  2. 在确定的升序区间中二分查找 target

三、第一步:找到最小值下标

使用 leftright 表示当前搜索范围,每次取中间位置 mid,并将 nums[mid]nums[right] 比较。

1. nums[mid] > nums[right]

复制代码
if (nums[mid] > nums[right]) {
    left = mid + 1;
}

这说明 mid 位于旋转点左侧的较大升序段中,最小值必然在 mid 的右边,因此可以排除 [left,mid]

例如:

复制代码
[4,5,6,7,0,1,2]
       ↑       ↑
      mid    right

此时 7 > 2,最小值一定在右半部分。

2. nums[mid] < nums[right]

复制代码
else if (nums[mid] < nums[right]) {
    right = mid;
}

这说明 mid 位于包含最小值的右侧升序段中,最小值可能正是 nums[mid],也可能在它左边,所以不能排除 mid,应令 right = mid

3. nums[mid] == nums[right]

复制代码
else {
    right--;
}

本题已说明数组元素互不相同,因此正常情况下不会进入这个分支。截图保留 right--,可以兼容数组存在重复元素的扩展场景:相等时无法判断最小值在哪一侧,只排除最右侧的一个重复元素。

循环条件使用:

复制代码
while (left < right)

left == right 时,搜索范围中只剩一个位置,该位置就是最小值下标。

四、第二步:判断目标值属于哪一段

找到 minIndex 后,旋转数组已经被划分成两个升序区间。接下来需要判断 target 应该在哪一段查找。

令:

复制代码
int right = nums.length - 1;

由于 nums[right] 是第二个升序区间的最大值,可以将 target 与它比较。

1. nums[right] < target

目标值大于第二段中的最大值,所以不可能在第二段,只能在第一段:

复制代码
return searchBinary(nums, target, 0, minIndex - 1);

2. nums[right] > target

结合题目的旋转结构,目标值若存在,应在以最小值开头的第二段中查找:

复制代码
return searchBinary(nums, target, minIndex, right);

如果 target 比数组最小值还小,虽然也会进入第二段搜索,但普通二分最终会返回 -1

3. nums[right] == target

最后一个元素正好就是目标值,可以直接返回:

复制代码
return right;

这种判断方式避免了额外比较 nums[0],并与截图中的实现保持一致。

五、第三步:在升序区间中二分查找

确定目标区间后,剩下的就是标准二分查找。

本文使用闭区间 [left,right],所以循环条件为:

复制代码
while (left <= right)

如果中间元素小于目标值,搜索右侧;如果中间元素大于目标值,搜索左侧;如果相等,直接返回 mid

复制代码
if (nums[mid] < target) {
    left = mid + 1;
} else if (nums[mid] > target) {
    right = mid - 1;
} else {
    return mid;
}

区间耗尽仍未命中,说明目标值不存在,返回 -1

六、完整 Java 代码

下面的代码严格采用截图中的"先找最小值,再选择区间,最后普通二分"的结构:

复制代码
class Solution {
    public int search(int[] nums, int target) {
        // 找到最小值下标,从而划分出两个升序区间
        int minIndex = findMin(nums);
        int right = nums.length - 1;

        // target 大于第二段最大值,只可能位于第一段
        if (nums[right] < target) {
            return searchBinary(nums, target, 0, minIndex - 1);
        } else if (nums[right] > target) {
            // target 若存在,应位于第二段
            return searchBinary(nums, target, minIndex, right);
        }

        // nums[right] == target
        return right;
    }

    // 在指定闭区间中执行标准二分查找
    private int searchBinary(int[] nums, int target, int left, int right) {
        while (left <= right) {
            int mid = left + ((right - left) >>> 1);

            if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid - 1;
            } else {
                return mid;
            }
        }

        return -1;
    }

    // 查找旋转数组中最小值所在的下标
    private int findMin(int[] nums) {
        int left = 0;
        int right = nums.length - 1;

        while (left < right) {
            int mid = left + ((right - left) >>> 1);

            if (nums[mid] > nums[right]) {
                // mid 位于左侧较大升序段
                left = mid + 1;
            } else if (nums[mid] < nums[right]) {
                // mid 位于包含最小值的升序段
                right = mid;
            } else {
                // 本题元素不重复;该分支用于兼容重复元素
                right--;
            }
        }

        return left;
    }
}

七、复杂度分析

findMin 使用二分查找定位最小值,时间复杂度为 O(log n)searchBinary 又执行一次二分查找,时间复杂度也是 O(log n)

两者顺序执行:

复制代码
O(log n) + O(log n) = O(log n)

算法只使用常数个变量,因此空间复杂度为 O(1)

需要注意:如果扩展到存在大量重复元素的数组,right-- 可能使 findMin 在最坏情况下退化为 O(n);但本题元素互不相同,不存在该问题。

八、常见错误

1. 找最小值时写成 right = mid - 1

nums[mid] < nums[right] 时,mid 本身可能就是最小值,所以必须保留它,写成 right = mid

2. 使用错误的循环条件

findMin 的目标是让两个指针相遇,应使用 left < right;普通二分使用闭区间,需要使用 left <= right

3. 第一段的右边界写成 minIndex

最小值属于第二段,第一段应为 [0,minIndex - 1]。否则两个区间会发生重叠。

4. 忘记处理未旋转数组

例如 [1,2,3,4] 的最小值下标是 0。此时第一段为 [0,-1],是空区间;若目标值大于 nums[right] 的情况不会出现,其余目标会进入第二段 [0,right],代码仍能正确运行。

九、总结

搜索旋转排序数组的关键,是认识到旋转只破坏了整体有序性,却保留了两个局部升序区间。

本解法先通过 findMin 定位最小值下标,将数组划分为两段;再比较 target 与最后一个元素,选择可能包含目标值的升序区间;最后使用标准二分查找得到答案。

相关推荐
言乐61 小时前
Python游戏水平测试辅助系统2
开发语言·windows·python·游戏·django
Web Security Loop1 小时前
MAC卸载JDK环境
java·macos·jdk
Forever Nore2 小时前
LeetCode 13 罗马数字转整数 - 按规则处理
linux·服务器·leetcode
Rain的Java大神之路2 小时前
线上接口负载满了如何解决
java·数据库·redis·后端·缓存·面试·架构
jerryinwuhan3 小时前
《Python数据预处理》第四章
开发语言·python
counting money3 小时前
Tomcat 专题:从基础架构到性能调优与 WebSocket 实战
java·websocket·tomcat
雨晨源码(同名B站)3 小时前
基于Python的网易云音乐评论数据情感化分析系统 音乐爬虫信息可视化 |SnowNLP评论情感分析
开发语言·hadoop·爬虫·python·信息可视化·毕业设计
阿童木写作3 小时前
跨境电商图片翻译工具推荐:跨马翻译批量处理视频字幕与抠图
python·音视频
xiaohaiAIgeo3 小时前
【2026年】HG/T 20656-2024化工暖通空调设计规范:新版标准的变化与影响
java·前端·javascript·科普知识