leetcode刷题记录-540. 有序数组中的单一元素 - 优化时间复杂度的二分查找

309 阅读3分钟

「这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战

前言

今日的题目为中等,主要是难在需要满足题目要求,顺带需要学习一下时间复杂度的概念,在平时做题的时候一般也是稍微会了解到这个东西的。

每日一题

今天的每日一题#### 540. 有序数组中的单一元素,难度为中等

  • 给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。

  • 请你找出并返回只出现一次的那个数。

  • 你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度。

示例 1:

输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2

示例 2:

输入: nums =  [3,3,7,7,10,11,11]
输出: 10
 

提示:

  • 1 <= nums.length <= 105
  • 0 <= nums[i] <= 105

题解

时间复杂度

实测哪怕是不考虑时间复杂度直接暴力解法也能够ac,但是既然题目要求了,那我们就要进行优化。

这道题的题目对时间复杂度提出了要求,那么首先就需要来了解一下时间复杂度,以及我们要怎么写才能实现我们需要的时间复杂度

什么是时间复杂度

时间复杂度简单的来说,就是去描述你这段代码运行所用的时间,比如我们最常见的 O(n) 就是时间复杂度为 n,说明你在代码中使用了一次for循环遍历了一个传入的变量n,或者说还有双重循环,比如:

for(let i=0;i<n;i++){
    for(let j=0;j<n;j++){
        ...
    }
}

这样的时间复杂度就是 O(n2)。

至于 O(log(N)) 我们拿一个经典的故事来解释一下:

传说西塔发明了国际象棋而使国王十分高兴,他决定要重赏西塔。西塔说:“我不要你的重赏,陛下,只要你在我的棋盘上赏一些麦子就行了。在棋盘的第1个格子里放1粒,在第2个格子里放2粒,在第3个格子里放4粒,在第4个格子里放8粒,依此类推,以 后每一个格子里放的麦粒数都是前一个格子里放的麦粒数的2倍,直到放满第64个格子就行了”。区区小数,几粒麦子,这有何难,“来人”,国王令人如数付给西塔。计数麦粒的工作开始了,第一格内放1粒,第二格内放2粒,第三格内放4粒。还没有到第二十格,一袋麦子已经空了。一袋又一袋的麦子被扛到国王面前来。但是,麦粒数一格接一格飞快增长着,国王很快就看出,即便拿出全国的粮食,也兑现不了他对西塔的诺言。

如此可见, O(log(N)) 其实是一个幂运算的过程,增加一倍的数量却只能增加一次运算,而这其实就是O(log(N))的本质:输入规模翻倍,操作次数只增加一

如何达到 O(log(N))

果要设计一个算法,让其具有 O(log(N)) 的时间复杂度,从正面思考是困难的。我们不妨想一想有没有什么操作是每操作一次,需要处理的规模就小一半的。

二分搜索,特别经典的例子。每次取中位数,在其左或其右继续搜索目标值。其本质就是每搜索一次,就把待搜索的数据量减小了一半。

所以这道题,我们也要思考,如果实现 O(log(N)) ,也就是每一次循环就排除掉一半的数据。

二分查找解题

关于唯一的那个数,出现的下标一定会是偶数,因为在它之前的数字都是一对一对出现的,而在它之后出现的数字,对数的位置相比于它前面的就发生了变化,所以这个就是这道题的关键点。

我们可以通过二分来查找中点位置的数的下标和他是和前面的数相等还是后面的数相等,这样就可以看出来需要的数是当前位置的前面还是后面。

判断出来以后,我们就可以更新数组的数量,保留存在的一半,另一半就可以舍弃,一直重复,知道找到我们需要的那个唯一的数字。

/**
 * @param {number[]} nums
 * @return {number}
 */
 var singleNonDuplicate = function(nums) {
    let l = 0, r = nums.length-1;
    while(l <= r) {
        let mid = l + Math.floor((r - l) / 2);
        mid -= mid % 2 === 1 ? 1 : 0;
        if (nums[mid] === nums[mid+1]) {
            l = mid + 2;
        } else {
            r = mid - 2;
        }
    }
    return nums[l];
};

image.png