[路飞]_leetcode刷题_128. 最长连续序列

157 阅读6分钟

题目

128. 最长连续序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:

输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。

示例 2:

输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9

解法一

思路:

直接可以想到的暴力解法有两种,如下:

  1. 先将数组排序,然后遍历数组,求最长的连续序列。这个思路仅排序就已经至少O(nlogn)了,所以可以直接排除。
  2. 遍历原始数组每一项,每遍历一项,都用它作为起始去再次遍历数组找num+1项,然后取最长的序列数,这个思路的时间复杂度度为O(n2),也超了。

那么基于暴力解法2,我们是有优化空间的。

思路如下:

  1. 用哈希集合先给数组去重,因为重复的数字对连续序列长度没有任何贡献,另外这样我们去查找集合中是否有某一项,时间复杂度为O(1),可以大大的增加效率。
  2. 遍历每一项,我们可以没必要急着去找它的num+1项,我们换个思路,我们判断它的num-1项是否存在于哈希集合,如果存在,我们等会用它的num-1项去往后找,长度肯定比num开始往后找大
  3. 如果集合中存在num-1,则跳过该次循环;如果集合中不存在,说明当前数字已经是连续数字里最小的数里,我们就从它开始往后找。

举个例子,[100,4,200,1,3,2]

  1. 先遍历100,发现99不存在,那么说明跟100连续的序列里100是最小的,从100开始往后找,判断是否存在101,不存在,则长度为1.
  2. 遍历4,发现3存在,则跳过
  3. 遍历200,发现199不存在,逻辑同遍历100
  4. 遍历1,发现0不存在,说明跟1连续的序列里1是最小的,从1开始往后找,判断2是否存在;存在,再判断3;存在,再判断4;存在,再判断5,不存在,跳出循环,长度为(4-1)+1 = 4;
  5. 再遍历3,发现2存在,跳过
  6. 遍历2,发现1存在,跳过

代码如下:

/**
 * @param {number[]} nums
 * @return {number}
 */
var longestConsecutive = function(nums) {
    let set = new Set(nums);
    let ans = 0;
    set.forEach((num)=>{
        let cur = num;
        if(!set.has(num-1)){
            while(set.has(num+1)){
                num++
            }
        }
        ans = Math.max(ans,num-cur+1)
    })
    return ans;
};

复杂度分析

时间复杂度:O(n),有人会觉得forEach里面套了while,怎么会是n呢,是由于我们有一个if判断,并不是每个元素都会走进while。我们举两个极端的例子:

  1. 数组为[1,2,3,4,5],forEach会遍历5次,第一次遍历1的时候,while会遍历5次,后面都不会进while,所以时间复杂度为O(2n),故为O(n)
  2. 数组为[1,3,5,7,9],forEach会遍历5次,每次遍历都不会进while,故为O(n)

所以时间复杂度为O(n)

空间复杂度:O(n),set存放长度最长为n。

解法二

思路

哈希表。

于解法一类似,我们可以把每个元素从自己开始的连续序列的有边界记录下来,存在哈希表里。我们遍历整个数组,去求每个元素的右边界的长度,取最大的返回即可。

代码如下

/**
 * @param {number[]} nums
 * @return {number}
 */
var longestConsecutive = function(nums) {
    let map = new Map();
    for(let i=0;i<nums.length;i++){
        map.set(nums[i],nums[i]);
    }
    let ans = 0;
    map.forEach((num)=>{
        let right = map.get(num);
        while(map.has(right+1)){
            right = map.get(right+1)
        }
        map.set(num,right);
        ans = Math.max(ans,right-num+1)
    })
    return ans;
};

复杂度分析

时间复杂度:O(n),分析同解法一。

空间复杂度:O(n),分析同解法一。

解法三

思路

并查集,我们可以将连续的序列看做一个连通量。

  1. 遍历nums数组,如果当前元素的下一项num+1存在,那么将当前元素绑定到以num+1为首的连通分量中。
  2. 再遍历nums数组,取每个元素对应的连通量里的顶级父元素,然后求出该连通分量的长度,每次遍历如果长度更大则更新长度。
  3. 返回长度ans

代码如下

/**
 * @param {number[]} nums
 * @return {number}
 */
var longestConsecutive = function(nums) {
    let parent = new Map();
    for(let num of nums){
        parent.set(num,num);
    }

    for(let num of nums){
        if(find(parent,num+1) != null){
            union(parent,num,num+1)
        }
    }
    let ans = 0;
    for(let num of nums){
        let right = find(parent,num)
        ans = Math.max(ans,right-num+1)
    }
    return ans;
};

function union(parent,p,q){
    let rootP = find(parent,p);
    let rootQ = find(parent,q);
    parent.set(rootP,rootQ);
}

function find(parent,x){
    if(!parent.has(x)){
        return null;
    }
    if(parent.get(x) !=x ){
        parent.set(x,find(parent,parent.get(x)))
    }
    return parent.get(x);
}

优化写法,将连通量的长度实时计算,这样可以少一次遍历

/**
 * @param {number[]} nums
 * @return {number}
 */
var longestConsecutive = function(nums) {
    if(nums.length == 0){
        return 0
    }
    let parent = new Map();
    let count = new Map();
    for(let num of nums){
        parent.set(num,num);
        count.set(num,1);
    }
    let ans = 1;
    for(let num of nums){
        if(find(parent,num+1) != null){
            ans = Math.max(ans,union(parent,count,num,num+1))
        }
    }
    return ans;
};

function union(parent,count,p,q){
    let rootP = find(parent,p);
    let rootQ = find(parent,q);
    if(rootP == rootQ){
        return count.get(rootQ);
    }
    parent.set(rootP,rootQ);
    count.set(rootQ,count.get(rootQ)+count.get(rootP))
    return count.get(rootQ);
}

function find(parent,x){
    if(!parent.has(x)){
        return null;
    }
    if(parent.get(x) !=x ){
        parent.set(x,find(parent,parent.get(x)))
    }
    return parent.get(x);
}

复杂度分析

时间复杂度:O(n),同解法一。

空间复杂度:O(n),同解法一。

解法四

思路

动态规划。

每一个元素num对应的包含它的区间的长度 = 以num-1为右边界的区间长度lenL + 以num+1为左边届的长度lenR,即 [num-lenL,num-1] num [num+1, num+lenR]

  1. 我们建立一个空的哈希表
  2. 遍历数组nums,如果当前元素不在哈希表内,说明该元素第一次出现,且它的num-1和num+1在计算长度的时候都没有算上过它,所以num对应的区间长度就为 num-1的长度 + num+1的长度 + 1
  3. 计算完该元素之后,更新一下目前这个区间的左边界和右边界对应的区间长度。

这个方法有点难理解,大家可以看着代码,拿纸笔画一下消化一下。

代码如下

/**
 * @param {number[]} nums
 * @return {number}
 */
var longestConsecutive = function(nums) {
        // key表示num,value表示num所在连续区间的长度
        let map = new Map();
        let ans = 0;
        for (let num of nums) {
            // 说明num第一次出现,这表示num-1和num+1一定分别是两个区间的边界
            // 即[x,num-1]和[num+1,y],
            // 否则num不可能第一次出现,一定会包含在两个区间其中一个或者两个都在。
            if (!map.has(num)) {
                // lenL为num-1所在连续区间的长度,即以num-1为右边界的区间长度
                let lenL = map.get(num - 1)?map.get(num - 1):0;
                // lenR为num+1所在连续区间的长度,即以num+1为左边界的区间长度
                let lenR = map.get(num + 1)?map.get(num + 1):0;
                // 当前连续区间的总长度
                let curLen = lenL + lenR + 1;
                ans = Math.max(ans, curLen);
                // 将num加入map中,表示已经遍历过该值。
                // 由于num已经不可能是边界了,也就是后面再计算其他num的lenL和lenR时不会再用到它了,所以值可以为任意值,
                // 这里我们还是赋值区间长度。
                map.set(num, curLen);
                // 更新当前连续区间左边界和右边界对应的区间长度
                map.set(num - lenL, curLen);
                map.set(num + lenR, curLen);
            }
        }
        return ans;
};

复杂度分析

时间复杂度:O(n)

空间复杂度:O(n)