【LeetCode Hot100 刷题日记 (66/100)】33. 搜索旋转排序数组 —— 二分查找的进阶应用🧠

6 阅读6分钟

📌 题目链接:33. 搜索旋转排序数组 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:数组、二分查找

⏱️ 目标时间复杂度:O(log n)

💾 空间复杂度:O(1)


在 LeetCode Hot100 的第 33 题中,我们面对一个看似“打乱”的升序数组——它被旋转了。但题目要求我们仍以 O(log n) 的时间复杂度完成查找,这直接排除了线性扫描的可能,必须使用二分查找

然而,这不是普通的有序数组,而是一个局部有序的旋转数组。如何在这种结构中依然高效地进行二分?这就是本题的核心考察点,也是面试中高频出现的经典变种题!


🧠 题目分析

给定一个原本严格升序、无重复元素的整数数组 nums,它在某个未知位置 k向左旋转(即前 k 个元素移到末尾),形成如 [4,5,6,7,0,1,2] 的结构。

任务:在 O(log n) 时间内判断目标值 target 是否存在,并返回其下标,否则返回 -1

✅ 关键约束:

  • 数组长度 ≥ 1
  • 所有元素 互不相同
  • 保证是一次旋转后的结果(不是多次打乱)

这意味着:整个数组由两个严格递增的子段拼接而成,且左段的所有元素 > 右段的所有元素(因为原数组升序)。

例如:

  • 原数组:[0,1,2,4,5,6,7]
  • 旋转后(k=3):[4,5,6,7,0,1,2]

这种结构虽然整体无序,但每一段内部有序,且可以通过中点判断哪一段是完整有序的——这正是我们能继续使用二分的关键!


⚙️ 核心算法及代码讲解:基于有序段判断的二分查找

📌 为什么还能用二分?

普通二分依赖“整个区间有序”来判断 target 在左还是右。
但在旋转数组中,每次取中点 mid 后,左右两半中必有一半是完全有序的

💡 证明:
假设旋转点为 k,那么数组分为 [0, k-1][k, n-1] 两段升序。
任取 mid,若 mid < k,则 [mid, n-1] 跨越了旋转点,无序;但 [0, mid] 完全在左段 → 有序
mid >= k,则 [0, mid] 跨越旋转点 → 无序;但 [mid, n-1] 完全在右段 → 有序
所以总有一侧是有序的

因此,我们的策略是:

  1. 计算 mid = (l + r) / 2

  2. 判断 nums[mid] == target?是 → 返回 mid

  3. 判断 左半段 [l, mid] 是否有序:通过 nums[l] <= nums[mid]

    • 如果有序:

      • target 落在 [nums[l], nums[mid]) 区间 → 搜索左半
      • 否则 → 搜索右半
    • 如果左半无序 → 则右半 [mid, r] 必有序

      • target 落在 (nums[mid], nums[r]] 区间 → 搜索右半
      • 否则 → 搜索左半

⚠️ 注意边界处理:由于元素唯一,可用 <=<,但需保持逻辑一致。


🧾 C++ 核心算法代码(带逐行注释)

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int n = (int)nums.size();
        if (!n) return -1;
        if (n == 1) return nums[0] == target ? 0 : -1;

        int l = 0, r = n - 1;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (nums[mid] == target) 
                return mid; // 找到目标,直接返回

            // 判断左半段 [l, mid] 是否有序
            if (nums[0] <= nums[mid]) {
                // 左半有序:检查 target 是否在左半范围内
                if (nums[0] <= target && target < nums[mid]) {
                    r = mid - 1; // 在左半,缩小右边界
                } else {
                    l = mid + 1; // 不在左半,去右半找
                }
            } else {
                // 左半无序 ⇒ 右半 [mid, r] 必有序
                if (nums[mid] < target && target <= nums[n - 1]) {
                    l = mid + 1; // 在右半,缩小左边界
                } else {
                    r = mid - 1; // 不在右半,去左半找
                }
            }
        }
        return -1; // 未找到
    }
};

✅ 注:这里用 nums[0] 作为左端参考是安全的,因为只要 nums[0] <= nums[mid],说明从 0 到 mid 没有跨越旋转点,即左段连续升序。


🧩 解题思路(分步拆解)

  1. 特判边界:空数组或单元素数组直接处理。

  2. 初始化双指针l = 0, r = n - 1

  3. 进入二分循环while (l <= r)):

    • 计算中点 mid

    • nums[mid] == target → 成功,返回 mid

    • 判断哪一半有序

      • nums[l] <= nums[mid] → 左半有序

        • 检查 target 是否在 [nums[l], nums[mid]) → 决定搜索方向
      • 否则 → 右半有序

        • 检查 target 是否在 (nums[mid], nums[r]] → 决定搜索方向
  4. 循环结束仍未找到 → 返回 -1

💬 面试 Tip:
面试官常会追问:“如果数组中有重复元素怎么办?”
答:此时 nums[l] == nums[mid] == nums[r] 无法判断哪边有序,最坏退化为 O(n),需特殊处理(如跳过重复)。但本题明确“元素唯一”,无需考虑。


📊 算法分析

项目分析
时间复杂度O(log n) —— 每次二分排除一半,标准二分效率
空间复杂度O(1) —— 仅用常数额外变量
稳定性稳定,因元素唯一,无歧义
适用场景旋转有序数组查找、部分有序结构搜索
面试价值⭐⭐⭐⭐⭐ 高频!考察对二分本质的理解,能否在“非完全有序”中识别“局部有序”

💻 完整可运行代码

✅ C++ 版本

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int n = (int)nums.size();
        if (!n) return -1;
        if (n == 1) return nums[0] == target ? 0 : -1;

        int l = 0, r = n - 1;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (nums[mid] == target) 
                return mid;

            if (nums[0] <= nums[mid]) {
                if (nums[0] <= target && target < nums[mid]) {
                    r = mid - 1;
                } else {
                    l = mid + 1;
                }
            } else {
                if (nums[mid] < target && target <= nums[n - 1]) {
                    l = mid + 1;
                } else {
                    r = mid - 1;
                }
            }
        }
        return -1;
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    Solution sol;
    
    // 测试用例 1
    vector<int> nums1 = {4,5,6,7,0,1,2};
    cout << sol.search(nums1, 0) << "\n"; // 输出: 4
    
    // 测试用例 2
    vector<int> nums2 = {4,5,6,7,0,1,2};
    cout << sol.search(nums2, 3) << "\n"; // 输出: -1
    
    // 测试用例 3
    vector<int> nums3 = {1};
    cout << sol.search(nums3, 0) << "\n"; // 输出: -1

    return 0;
}

✅ JavaScript 版本

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var search = function(nums, target) {
    const n = nums.length;
    if (n === 0) return -1;
    if (n === 1) return nums[0] === target ? 0 : -1;

    let l = 0, r = n - 1;
    while (l <= r) {
        const mid = Math.floor((l + r) / 2);
        if (nums[mid] === target) return mid;

        // 判断左半是否有序
        if (nums[0] <= nums[mid]) {
            if (nums[0] <= target && target < nums[mid]) {
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        } else {
            // 右半有序
            if (nums[mid] < target && target <= nums[n - 1]) {
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
    }
    return -1;
};

// 测试用例
console.log(search([4,5,6,7,0,1,2], 0)); // 4
console.log(search([4,5,6,7,0,1,2], 3)); // -1
console.log(search([1], 0));             // -1

🎯 面试高频追问 & 拓展思考

  1. Q:如果数组中有重复元素,还能 O(log n) 吗?
    A:不能保证。当 nums[l] == nums[mid] == nums[r] 时,无法判断哪边有序,需 l++r-- 跳过,最坏 O(n)。
  2. Q:如何找到旋转点(最小值位置)?
    A:类似思路,找 nums[mid] > nums[mid+1] 的位置,或利用 nums[mid] < nums[r] 判断右半有序。
  3. Q:这个思想还能用在哪些题?
    A:LeetCode 153(寻找旋转排序数组中的最小值)、81(含重复的搜索)、162(峰值元素)等。

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!