题目
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
解法一
思路:
直接可以想到的暴力解法有两种,如下:
- 先将数组排序,然后遍历数组,求最长的连续序列。这个思路仅排序就已经至少O(nlogn)了,所以可以直接排除。
- 遍历原始数组每一项,每遍历一项,都用它作为起始去再次遍历数组找num+1项,然后取最长的序列数,这个思路的时间复杂度度为O(n2),也超了。
那么基于暴力解法2,我们是有优化空间的。
思路如下:
- 用哈希集合先给数组去重,因为重复的数字对连续序列长度没有任何贡献,另外这样我们去查找集合中是否有某一项,时间复杂度为O(1),可以大大的增加效率。
- 遍历每一项,我们可以没必要急着去找它的num+1项,我们换个思路,我们判断它的num-1项是否存在于哈希集合,如果存在,我们等会用它的num-1项去往后找,长度肯定比num开始往后找大
- 如果集合中存在num-1,则跳过该次循环;如果集合中不存在,说明当前数字已经是连续数字里最小的数里,我们就从它开始往后找。
举个例子,[100,4,200,1,3,2]
- 先遍历100,发现99不存在,那么说明跟100连续的序列里100是最小的,从100开始往后找,判断是否存在101,不存在,则长度为1.
- 遍历4,发现3存在,则跳过
- 遍历200,发现199不存在,逻辑同遍历100
- 遍历1,发现0不存在,说明跟1连续的序列里1是最小的,从1开始往后找,判断2是否存在;存在,再判断3;存在,再判断4;存在,再判断5,不存在,跳出循环,长度为(4-1)+1 = 4;
- 再遍历3,发现2存在,跳过
- 遍历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,2,3,4,5],forEach会遍历5次,第一次遍历1的时候,while会遍历5次,后面都不会进while,所以时间复杂度为O(2n),故为O(n)
- 数组为[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),分析同解法一。
解法三
思路
并查集,我们可以将连续的序列看做一个连通量。
- 遍历nums数组,如果当前元素的下一项num+1存在,那么将当前元素绑定到以num+1为首的连通分量中。
- 再遍历nums数组,取每个元素对应的连通量里的顶级父元素,然后求出该连通分量的长度,每次遍历如果长度更大则更新长度。
- 返回长度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]
- 我们建立一个空的哈希表
- 遍历数组nums,如果当前元素不在哈希表内,说明该元素第一次出现,且它的num-1和num+1在计算长度的时候都没有算上过它,所以num对应的区间长度就为 num-1的长度 + num+1的长度 + 1
- 计算完该元素之后,更新一下目前这个区间的左边界和右边界对应的区间长度。
这个方法有点难理解,大家可以看着代码,拿纸笔画一下消化一下。
代码如下
/**
* @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)