LeetCode 33. 搜索旋转排序数组:O(log n)二分查找

在LeetCode中等难度题目中,「搜索旋转排序数组」是一道经典的二分查找变形题。它的核心考点的是对"旋转数组"特性的理解,以及如何在非完全升序的数组中,依然保持二分查找O(log n)的时间复杂度。今天就来一步步拆解这道题,从题目分析到代码实现,再到细节注意点,帮你彻底搞懂它。

一、题目解读:什么是旋转排序数组?

题目给出的前提很明确:

  • 原数组是升序排列 的,且所有元素互不相同(这一点很关键,避免了重复元素带来的判断干扰);

  • 数组在某个未知下标k处「向左旋转」,旋转后数组变成 nums\[k, numsk+1, ..., numsn-1, nums0, nums1, ..., numsk-1];

  • 给定旋转后的数组nums和目标值target,要求找到target的下标,找不到则返回-1,且必须满足O(log n)时间复杂度。

举个例子:原数组 0,1,2,4,5,6,7,在k=3处向左旋转后,得到 4,5,6,7,0,1,2。如果target=0,返回下标4;如果target=3,返回-1。

这里的核心矛盾是:数组不再是完全升序,但又保留了"部分升序"的特性------旋转后数组会被分成两个升序子数组(比如例子中的 4,5,6,70,1,2)。而二分查找的核心是"通过中间值缩小查找范围",所以我们的思路就是利用这种"部分升序",判断target落在哪个升序区间,进而调整双指针。

二、核心思路:二分查找的变形应用

常规二分查找适用于完全升序数组,通过mid值与target的大小对比,直接调整左右指针。但旋转数组有两个升序区间,所以我们需要先判断「mid所在的区间是否是升序区间」,再判断target是否在该区间内,从而确定指针移动方向。

具体步骤拆解:

  1. 初始化双指针l=0(左边界)、r=n-1(右边界),n为数组长度;

  2. 循环条件:l ≤ r(当l超过r时,说明查找范围为空,未找到target);

  3. 计算mid = Math.floor((l + r) / 2)(注意TS/JS中除法会返回浮点数,需手动取整);

  4. 若numsmid === target,直接返回mid(找到目标值);

  5. 判断mid所在的区间是否为升序:

    • 情况1:nums0 ≤ numsmid → mid左侧(l到mid)是升序区间;

    • 情况2:nums0 > numsmid → mid右侧(mid到r)是升序区间;

  6. 根据升序区间,判断target是否在该区间内,调整指针:

    • 情况1(左侧升序):若target在nums\[0, numsmid)之间 → 缩小范围到左侧(r=mid-1);否则缩小到右侧(l=mid+1);

    • 情况2(右侧升序):若target在(numsmid, numsn-1]之间 → 缩小范围到右侧(l=mid+1);否则缩小到左侧(r=mid-1);

  7. 循环结束后仍未找到,返回-1。

三、完整代码实现(TypeScript)

结合上述思路,给出完整的TypeScript代码,关键步骤已添加注释,方便理解:

typescript 复制代码
function search(nums: number[], target: number): number {
    const n = nums.length;
    // 边界处理:数组为空,直接返回-1
    if (n === 0) {
        return -1;
    }
    // 边界处理:数组只有一个元素,直接判断是否等于target
    if (n === 1) {
        return nums[0] === target ? 0 : -1;
    }
    // 初始化双指针:左指针l,右指针r
    let l = 0, r = n - 1;
    // 二分查找循环:当左指针不大于右指针时,继续查找
    while (l <= r) {
        // 计算中间下标mid,手动取整避免浮点数
        const mid = Math.floor((l + r) / 2);
        // 找到目标值,直接返回下标
        if (nums[mid] === target) {
            return mid;
        }
        // 情况1:mid左侧是升序区间(nums[0] <= nums[mid])
        if (nums[0] <= nums[mid]) {
            // 判断target是否在左侧升序区间内(nums[0] <= target < nums[mid])
            if (nums[0] <= target && target < nums[mid]) {
                // 缩小范围到左侧,右指针左移
                r = mid - 1;
            } else {
                // 缩小范围到右侧,左指针右移
                l = mid + 1;
            }
        } else {
            // 情况2:mid右侧是升序区间(nums[0] > nums[mid])
            // 判断target是否在右侧升序区间内(nums[mid] < target <= nums[n-1])
            if (nums[mid] < target && target <= nums[n - 1]) {
                // 缩小范围到右侧,左指针右移
                l = mid + 1;
            } else {
                // 缩小范围到左侧,右指针左移
                r = mid - 1;
            }
        }
    }
    // 循环结束,未找到目标值,返回-1
    return -1;
}

四、关键细节与易错点

这道题的代码不算复杂,但细节处理不到位很容易出错,尤其是以下3个点:

1. 边界条件处理

必须先处理数组为空(n=0)和数组只有一个元素(n=1)的情况。如果忽略这两个边界,当数组长度为0时会出现指针异常,长度为1时会进入循环做无用功,影响效率。

2. mid的计算方式

在TypeScript/JavaScript中,(l + r) / 2 会返回浮点数(比如l=0、r=1时,结果是0.5),所以必须用Math.floor()取整,否则mid会是小数,导致数组下标报错。

补充:也可以用 (l + r) >> 1 进行位运算取整(效果等同于Math.floor((l + r)/2)),但要注意避免溢出(本题中数组长度不会过大,两种方式均可)。

3. 区间判断的等号问题

这是最容易出错的地方,比如:

  • 左侧升序区间判断时,用 nums0 ≤ numsmid(包含等于,因为mid可能就是0下标,此时左侧只有一个元素,也是升序);

  • target在左侧区间的判断的是 nums0 ≤ target && target < numsmid(target不能等于numsmid,因为前面已经判断过numsmid !== target);

  • 右侧升序区间判断时,target的范围是 numsmid < target && target ≤ numsn-1(同理,target不能等于numsmid)。

如果等号位置写错,会导致指针调整错误,进而错过目标值或者进入死循环。

五、复杂度分析与题目延伸

1. 时间复杂度

整个算法采用二分查找,每次循环都会将查找范围缩小一半,所以时间复杂度是 O(log n),完全满足题目要求。

2. 空间复杂度

算法只使用了常数个变量(l、r、mid、n),没有使用额外的空间,空间复杂度是 O(1)。

3. 题目延伸

这道题的变形题是「搜索旋转排序数组II」,区别在于数组元素可以重复。此时,nums0 ≤ numsmid 无法直接判断左侧是升序区间(比如 1,0,1,1,1),需要先处理重复元素(比如当numsl === numsmid时,l++),感兴趣的可以后续深入研究。

六、总结

「搜索旋转排序数组」的核心是"利用旋转数组的部分升序特性,改造二分查找"。解题的关键在于:

  1. 判断mid所在的升序区间;

  2. 根据target是否在该升序区间,调整双指针;

  3. 注意边界条件和等号的处理。

这道题虽然是中等难度,但只要掌握了二分查找的核心思想,再结合旋转数组的特性,就能轻松破解。建议大家多动手调试代码,尝试不同的测试用例(比如旋转点在开头、结尾、中间的情况),加深对算法的理解。

相关推荐
李明卫杭州1 分钟前
CSS aspect-ratio 属性完全指南
前端
Pedantic2 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘2 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆2 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师3 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆3 小时前
VSCode自动格式化三要素
前端
爱勇宝4 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen5 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
user20585561518137 小时前
Windows 项目安装时报 `node-sass` 错误,如何快速处理
前端