【月度刷题计划同款】详解为何元素相同会导致 O(n),一起看清二分的本质

题目描述

这是 LeetCode 上的 81. 搜索旋转排序数组 II ,难度为 中等

Tag : 「二分」

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

在传递给函数之前,nums 在预先未知的某个下标 k( <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 < = k < n u m s . l e n g t h 0 <= k < nums.length </math>0<=k<nums.length)上进行了 旋转 ,使数组变为 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ n u m s [ k ] , n u m s [ k + 1 ] , . . . , n u m s [ n − 1 ] , n u m s [ 0 ] , n u m s [ 1 ] , . . . , n u m s [ k − 1 ] ] [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]] </math>[nums[k],nums[k+1],...,nums[n−1],nums[0],nums[1],...,nums[k−1]](下标从 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 开始计数)。

例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4]

给你 旋转后 的数组 nums 和一个整数 target,请你编写一个函数来判断给定的目标值是否存在于数组中。

如果 nums 中存在这个目标值 target,则返回 true,否则返回 false

示例 1:

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

输出:true

示例 2:

ini 复制代码
输入:nums = [2,5,6,0,0,1,2], target = 3

输出:false

提示:

  • <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 < = n u m s . l e n g t h < = 5000 1 <= nums.length <= 5000 </math>1<=nums.length<=5000
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> − 1 0 4 < = n u m s [ i ] < = 1 0 4 -10^4 <= nums[i] <= 10^4 </math>−104<=nums[i]<=104
  • 题目数据保证 nums 在预先未知的某个下标上进行了旋转
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> − 1 0 4 < = t a r g e t < = 1 0 4 -10^4 <= target <= 10^4 </math>−104<=target<=104

二分

根据题意,我们知道,所谓的旋转其实就是「将某个下标前面的所有数整体移到后面,使得数组从整体有序变为分段有序」。

但和 33. 搜索旋转排序数组 不同的是,本题元素并不唯一。

这意味着我们无法直接根据与 <math xmlns="http://www.w3.org/1998/Math/MathML"> n u m s [ 0 ] nums[0] </math>nums[0] 的大小关系,将数组划分为两段,即无法通过「二分」来找到旋转点。

因为「二分」的本质是二段性,并非单调性。只要一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。

如果你有看过我 33. 搜索旋转排序数组 这篇题解,你应该很容易就理解上句话的意思。如果没有也没关系,我们可以先解决本题,在理解后你再去做 33. 搜索旋转排序数组,我认为这两题都是一样的,不存在先后关系。

举个🌰,我们使用数据 [0,1,2,2,2,3,4,5] 来理解为什么不同的旋转点会导致「二段性丢失」:

代码:

java 复制代码
class Solution {
    public boolean search(int[] nums, int t) {
        int n = nums.length;
        int l = 0, r = n - 1;
        // 恢复二段性
        while (l < r && nums[0] == nums[r]) r--;

        // 第一次二分,找旋转点
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] >= nums[0]) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }
        
        int idx = n;
        if (nums[r] >= nums[0] && r + 1 < n) idx = r + 1;

        // 第二次二分,找目标值
        int ans = find(nums, 0, idx - 1, t);
        if (ans != -1) return true;
        ans = find(nums, idx, n - 1, t);
        return ans != -1;
    }
    int find(int[] nums, int l, int r, int t) {
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= t) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return nums[r] == t ? r : -1;
    }
}
  • 时间复杂度:恢复二段性处理中,最坏的情况下(考虑整个数组都是同一个数)复杂度是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n),而之后的找旋转点和目标值都是「二分」,复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( log ⁡ n ) O(\log{n}) </math>O(logn)。整体复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) 的。
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)。

最后

这是我们「刷穿 LeetCode」系列文章的第 No.81 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour...

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉

相关推荐
想用offer打牌12 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
崔庆才丨静觅13 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606113 小时前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX13 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了13 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅14 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅14 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法14 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
崔庆才丨静觅14 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端