力扣热题100实战 | 第33期:搜索旋转排序数组——二分查找的变体艺术

力扣热题100实战 | 第33期:搜索旋转排序数组------二分查找的变体艺术

当有序数组被旋转,二分查找还能用吗?答案是肯定的,核心在于判断哪一半是单调的。这道题教会我们:在看似混乱的数据中,只要找到一个有序的片段,就能继续用二分缩小范围。

前言

你好,我是@礼拜天没时间。

上一期我们攻克了"最长有效括号"(第32题),掌握了动态规划和栈在括号匹配中的应用。这一期,我们来解一道二分查找的经典变体------搜索旋转排序数组(LeetCode 第33题)。

这道题在力扣上被标记为"中等",但它却是二分查找思想应用的最佳范例。很多人学完经典二分后,遇到数组被"旋转"一下就不知道该怎么下手了。其实,只要我们能够识别出数组的"部分有序"性质,二分查找依然能够大显身手。

今天,我希望能带你从最朴素的线性搜索出发,一步步推导出二分查找的核心逻辑,并掌握处理边界条件和重复元素的技巧。


一、题目:在旋转数组中找目标值

先看题目描述(LeetCode 第33题):

整数数组 nums 按升序排列,数组中的值 互不相同

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]。例如, [0,1,2,4,5,6,7] 在下标 3 处旋转后变成 [4,5,6,7,0,1,2]

给你旋转后的数组 nums 和一个整数 target ,如果 target 在数组中,则返回它的下标,否则返回 -1

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

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

示例 2:

复制代码
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1

示例 3:

复制代码
输入:nums = [1], target = 0
输出:-1

关键点解读

  1. 唯一性 :数组中的值互不相同,这简化了问题。进阶版(第81题)允许重复元素,会更复杂。

  2. 旋转定义:本质就是把原有序数组的前面一部分(长度为k)移到末尾,形成"两段有序"的结构。

  3. 时间复杂度要求 :O(log n),这直接暗示我们要用二分查找

  4. 寻找旋转点:不一定需要显式找到旋转点,可以在二分过程中动态判断哪一半是有序的。


二、第一反应:线性扫描(违反要求)

当我第一次看到这道题,我的第一反应是:直接遍历一遍,找到目标值返回下标

java 复制代码
class Solution {
    public int search(int[] nums, int target) {
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] == target) return i;
        }
        return -1;
    }
}
  • 时间复杂度:O(n) ------ 不符合题目要求
  • 空间复杂度:O(1)

虽然能过测试,但题目明确要求 O(log n),必须用二分。


三、核心解法:二分查找(判断有序半区)

3.1 核心思想

因为数组原本有序,只是被旋转了一次,所以它有一个重要的性质:对于任意中间点 mid,数组的左半部分 left, mid 和右半部分 mid+1, right 至少有一个是严格递增的

我们可以通过比较 nums[left]nums[mid] 来判断哪一边是有序的:

  • 如果 nums[left] <= nums[mid],说明左半部分 left, mid 是递增的(没有旋转点落在这一区间)。
  • 否则,右半部分 mid+1, right 是递增的。

确定了有序半区之后,我们就可以判断目标值是否在这个有序半区内:

  • 如果在,则继续在这个半区内二分。
  • 如果不在,则目标在另一半(无序半区),继续二分。

3.2 算法步骤

  1. 初始化 left = 0, right = nums.length - 1
  2. left <= right 时循环:
    • 计算 mid = left + (right - left) / 2
    • 如果 nums[mid] == target,直接返回 mid
    • 判断左半部分是否有序:if (nums[left] <= nums[mid])
      • 若左半有序,检查 target 是否在 [nums[left], nums[mid]) 范围内:
        • 如果是,则 right = mid - 1
        • 否则,left = mid + 1
    • 否则,右半部分有序:
      • 检查 target 是否在 (nums[mid], nums[right]] 范围内:
        • 如果是,则 left = mid + 1
        • 否则,right = mid - 1
  3. 循环结束返回 -1

3.3 图解流程

nums = [4,5,6,7,0,1,2], target = 0 为例:

第1步left=0, right=6, mid=3

复制代码
nums = [4,5,6,7,0,1,2]
        ↑     ↑     ↑
       left  mid   right
  • nums[left]=4 <= nums[mid]=7 → 左半 4,5,6,7 有序。
  • target=0 不在 4,7 内 → 去右半 left=mid+1=4

第2步left=4, right=6, mid=5

复制代码
nums = [4,5,6,7,0,1,2]
                    ↑
                 left,mid?
准确下标:left=4(0), right=6(2), mid=5(1)
  • nums[left]=0 <= nums[mid]=1 → 左半 0,1 有序。
  • target=0 在 0,1 内 → right=mid-1=4

第3步left=4, right=4, mid=4

  • nums[mid]=0 == target → 返回4 ✅

3.4 代码实现

java 复制代码
class Solution {
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid;
            }
            
            // 判断左半部分是否有序
            if (nums[left] <= nums[mid]) {
                // 左半部分有序
                if (target >= nums[left] && target < nums[mid]) {
                    right = mid - 1;
                } else {
                    left = mid + 1;
                }
            } else {
                // 右半部分有序
                if (target > nums[mid] && target <= nums[right]) {
                    left = mid + 1;
                } else {
                    right = mid - 1;
                }
            }
        }
        return -1;
    }
}

3.5 复杂度分析

  • 时间复杂度:O(log n) ------ 每次循环将区间缩小一半
  • 空间复杂度:O(1) ------ 只用了常数变量

四、细节剖析:面试官真正关心的问题

Q1:为什么条件是 nums[left] <= nums[mid] 而不是 <

答案 :当 left == mid 时(即区间内只有1或2个元素),nums[left] <= nums[mid] 成立,此时左半部分也是有序的(单个元素自然有序)。如果用 <,可能会在 left == mid 时误判,导致逻辑错误。

Q2:如何判断目标值是否在有序半区?为什么有的用 < 有的用 <=

答案 :因为我们已经检查过 nums[mid] == target,所以:

  • 对于左半部分有序,范围是 [nums[left], nums[mid]),左闭右开,因为 mid 已经排除。
  • 对于右半部分有序,范围是 (nums[mid], nums[right]],左开右闭。
    这样能避免重复判断 mid 位置。

Q3:如果数组没有旋转(k=0),算法还能正确工作吗?

答案 :能。此时 nums[left] <= nums[mid] 始终成立,算法会一直走左半有序分支,等同于在完全有序数组上进行普通二分查找。

Q4:为什么不需要显式找到旋转点?

答案 :因为旋转信息隐含在比较 nums[left]nums[mid] 的结果中。我们不需要提前知道旋转点位置,只需要在每次二分时动态判断哪一半有序,然后根据 target 决定下一步搜索区间。

Q5:如果数组中有重复元素(LeetCode 81),这个算法还适用吗?

答案 :不能直接适用,因为当 nums[left] == nums[mid] 时,无法判断哪一半有序,需要退化到线性扫描或特殊处理。LeetCode 81 提供了进阶版本。


五、面试官追问进阶版

追问1:如何实现一个方法找到旋转数组中的最小值(LeetCode 153)?

思路 :同样用二分,但比较的是 nums[mid]nums[right]。如果 nums[mid] > nums[right],说明最小值在右半;否则在左半(包含 mid)。类似地,也可以比较 nums[left]nums[mid]

java 复制代码
public int findMin(int[] nums) {
    int left = 0, right = nums.length - 1;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > nums[right]) {
            left = mid + 1;
        } else {
            right = mid;
        }
    }
    return nums[left];
}

追问2:如果数组旋转了多次(其实等价于一次旋转),是否会影响正确性?

答案:不会。任意次旋转等价于一次旋转(因为旋转是循环移动),我们的算法依然有效。

追问3:如何用递归实现二分?

思路:递归分解子问题,本质上与迭代相同,但递归深度 O(log n) 会有额外空间开销。

追问4:如果数组长度非常大(10⁷),这个二分法仍然高效吗?

答案:是的,二分查找的对数复杂度在数据量极大时优势非常明显。10⁷ 的数据量只需约 24 次比较即可完成。


六、实际开发:这道题到底有什么用?

很多读者会问:"旋转数组中查找,实际工作中哪用得到?"

其实它的思想无处不在:

场景1:日志索引查询

日志系统常常按时间戳排序存储,但可能因为系统故障导致部分日志被移动到其他位置(相当于旋转)。二分查找的思想能快速定位特定时间点的日志。

场景2:循环队列中的查找

循环队列在物理上是线性的,但逻辑上是环形的。在环形缓冲中查找元素,可以转化为旋转数组的查找。

场景3:数据库索引中的断裂页

某些数据库索引中,数据页可能因为整理导致顺序被打乱但依然是部分有序,二分查找变体可以用于在这些段中快速定位。

场景4:游戏开发中的物品栏排序

游戏中的背包物品可能按某种顺序排列,但用户可以对物品进行旋转排序(如把前面的物品移到后面),查找特定物品时可以用类似思想。

场景5:算法面试题

这道题是二分查找变体中最经典的考题,考察对二分思想的深刻理解。


七、总结:从一道题到一类题

回顾一下,我们从搜索旋转排序数组学到了什么:

维度 收获
算法思维 线性扫描 → 二分查找,理解"部分有序"性质如何被利用
代码技巧 动态判断有序半区、区间端点比较的细节
复杂度分析 O(log n) 时间、O(1) 空间,最优解
面试要点 为什么要判断有序半区?条件为什么是 <=?如何处理边界?
工程关联 日志索引、环形缓冲、数据库索引、背包管理

力扣热题100的第三十三题,不是为了难住你,而是为了告诉你:二分查找的舞台远不止完全有序的数组。学会识别问题中的"部分有序",你就能让二分查找的光芒在不完美中继续闪耀。

下一期预告:《在排序数组中查找元素的第一个和最后一个位置》------二分查找的两次运用


附录:思考题

看完这篇文章,你可以试着回答:

  1. 如果数组中允许重复元素(LeetCode 81),上述算法需要怎么调整?
  2. 如何用这个思想解决"旋转数组中的峰值查找"问题?
  3. 你能用这道题的思路,去解 LeetCode 153(寻找旋转排序数组中的最小值)和 LeetCode 154(寻找旋转排序数组中的最小值 II)吗?

欢迎在评论区留下你的思考!

相关推荐
江畔柳前堤8 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore10 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考11 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX12 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面12 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号13 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽13 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI13 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者15 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者15 小时前
【征程6】校准量化中HistogramObserver解析
算法