#81 二分查找 中等

搜索旋转排序数组 II

在 LeetCode 上查看 ↗

🎧 语音讲解

开车或通勤时可听,跟着思路走一遍

速度

📋 题目描述

已知存在一个按非降序排列的整数数组 nums,数组中的值不必互不相同。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]。

给你 旋转后 的数组 nums 和一个整数 target,请你判断 target 是否存在于数组中:存在返回 true,否则返回 false。请尽可能减少操作步骤。

示例 1

输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true
0 在数组中出现。

示例 2

输入:nums = [2,5,6,0,0,1,2], target = 3
输出:false
数组中不存在 3。

💭 模拟答题者思考

1. 暴力:从左到右扫一遍看有没有 target,O(n)——能判断存在性,但题目希望尽量快,且与旋转有序结构无关。

2. 重复在哪里?和 #33 一样,旋转后任意 mid 至少有一半仍是原数组的非降序段;可以在那一半上用二分思想缩区间。但本题允许重复,nums[l] == nums[mid] == nums[r] 时无法区分「左半有序」还是「右半有序」。

3. 关键转化:先处理三端相等——同时 l++、r--,去掉两个与 mid 同值的端点(它们不可能单独成为唯一答案的判据)。其余情况照 #33:判哪半非降序,再看 target 是否落在该半的数值范围内。

4. 例 [2,5,6,0,0,1,2], target=0:二分过程中会在右半非降序段 [0,0,1,2] 内命中;若遇到 [1,0,1,1,1], target=0 这类重复密集区间,三端相等分支会线性挤边界,最坏 O(n)。

5. 返回布尔值:找到任意一个 nums[mid]==target 即可 true,无需记录下标;搜完区间仍无则 false。

🧠 变量语义(先读这三句再编码)

变量类型语义(三句法)
l, rint定义:当前待搜索区间的左右边界
维护:若 target 存在,至少有一个下标落在 [l, r] 内
更新:正常二分缩半;或在「三端相等」歧义时同步 l += 1、r -= 1 挤掉无法判定的重复值
midint定义:区间中点 (l + r) // 2
维护:将区间切成左段 [l, mid] 与右段 [mid+1, r];在非歧义时至少一段仍为非降序
更新:每轮重算;nums[mid] == target 时立即返回 true
歧义收缩—定义:当 nums[l] == nums[mid] == nums[r] 时,无法判断哪一半仍保持原非降序
维护:此时不能安全地丢掉任一半区间,只能排除端点重复值
更新:l += 1 且 r -= 1 后 continue,进入下一轮
有序半段判定bool定义:若 nums[l] <= nums[mid],认为左半 [l, mid] 非降序;否则右半 [mid+1, r] 非降序
维护:与 #33 相同,但相等元素会使「严格升序」退化为「非降序」,边界比较用 <=
更新:在有序半段上用 nums[l] <= target < nums[mid] 或 nums[mid] < target <= nums[r] 决定缩哪边

⌨️ 落码步骤

1. l = 0,r = len(nums) - 1

2. 当 l <= r:取 mid,若 nums[mid] == target 返回 true

3. 若 nums[l] == nums[mid] == nums[r]:l += 1,r -= 1,继续下一轮

4. 若 nums[l] <= nums[mid](左半非降序):nums[l] <= target < nums[mid] 则 r = mid - 1,否则 l = mid + 1

5. 否则(右半非降序):nums[mid] < target <= nums[r] 则 l = mid + 1,否则 r = mid - 1

6. 循环结束返回 false

💻 代码实现

class Solution:
    def search(self, nums: list[int], target: int) -> bool:
        l, r = 0, len(nums) - 1
        while l <= r:
            mid = (l + r) // 2
            if nums[mid] == target:
                return True
            if nums[l] == nums[mid] == nums[r]:
                l += 1
                r -= 1
                continue
            if nums[l] <= nums[mid]:          # 左半 [l, mid] 非降序
                if nums[l] <= target < nums[mid]:
                    r = mid - 1
                else:
                    l = mid + 1
            else:                             # 右半 [mid+1, r] 非降序
                if nums[mid] < target <= nums[r]:
                    l = mid + 1
                else:
                    r = mid - 1
        return False
class Solution {
public:
    bool search(vector<int>& nums, int target) {
        int l = 0, r = (int)nums.size() - 1;
        while (l <= r) {
            int mid = l + (r - l) / 2;
            if (nums[mid] == target) return true;
            if (nums[l] == nums[mid] && nums[mid] == nums[r]) {
                l++;
                r--;
                continue;
            }
            if (nums[l] <= nums[mid]) {      // 左半 [l, mid] 非降序
                if (nums[l] <= target && target < nums[mid])
                    r = mid - 1;
                else
                    l = mid + 1;
            } else {                         // 右半 [mid+1, r] 非降序
                if (nums[mid] < target && target <= nums[r])
                    l = mid + 1;
                else
                    r = mid - 1;
            }
        }
        return false;
    }
};
// 均摊 O(log n),重复极多时最坏 O(n);空间 O(1)

📈 复杂度分析

时间复杂度 O(log n) 均摊,最坏 O(n)
空间复杂度 O(1)

⚠️ 常见坑

❗ 漏掉三端相等分支:直接套 #33 会在 nums[l]==nums[mid]==nums[r] 时误判有序半段,可能错误缩区间导致漏答。

❗ 本题是非降序而非严格升序,左半有序时仍用 target < nums[mid](不能写成 <=),因为 nums[mid]==target 已在前面返回。

❗ 返回值是布尔型:未找到应返回 false,不要返回 -1;找到一处即可 true,无需继续搜其他重复位置。

🔍 必测边界 Case

Case 1:单元素命中
nums = [1], target = 1 → true
Case 2:全数组相同值
nums = [2,2,2,2], target = 2 → true(大量三端相等,靠缩边界与命中 mid)
Case 3:重复值与旋转断点
nums = [2,5,6,0,0,1,2], target = 0 → true
Case 4:存在相同端点导致歧义
nums = [1,0,1,1,1], target = 0 → true(需三端相等时 l++/r-- 才能继续二分)