题目:
给定一个未排序的整数数组,找出最长连续序列的长度。
要求算法的时间复杂度为 O(n)。
示例:
输入: [100, 4, 200, 1, 3, 2]
输出: 4
解释: 最长连续序列是 [1, 2, 3, 4]。它的长度为 4。
分析
要找 连续序列 的长度,从结果可以看出,主要是为了排序,排序之后,遍历结果就可以进行连续长度的更新。
但是排序的算法,即使快排的时间复杂度也有O(nlogn),不满足要求。
这种时候一般就会想到借助哈希表,也就是类似计数排序,先来试试,用数组来做哈希表,下标做nums[i]。
1. 找出最大最小值(考虑有负值),这样就确定了哈希表的size
2. 遍历数组,对应的值在哈希表里++;
3. 遍历哈希表,非 0 元素连续长度的最大值,进行更新。
class Solution {
public int longestConsecutive(int[] nums) {
int max=0,min=0;
for(int num: nums){
max=Math.max(max,num);
min=Math.min(min,num);
}
min=-min;
int[] bucket=new int[max+min+1];
for(int num: nums){
bucket[num+min]++;
}
int ans=0,temp=0;
int i=0;
while(i<max+min+1){
while(i<max+min+1 && bucket[i]!=0){
temp++;
i++;
}
ans=Math.max(ans,temp);
temp=0;
i++;
}
return ans;
}
}
提交之后给我当头一棒:
最后执行的输入:
[2147483646,-2147483647,0,2,2147483644,-2147483645,2147483645]
可以看到,涉及到了整型的最小值和最大值问题,如果直接计算要使用的 bucket 的长度,一定会溢出,并且如果输入里有 int 的最小值,取反之后也要处理溢出。
上一种做法是,全部存下来,再去判断,而数组按照元素大小是连续存储的,这样肯定会有很多空间的浪费。
但是哈希的想法本身应该是没有问题的,我们来想想这个过程如何优化。
换成使用 HashSet 来存储本来的元素,这样第二次进行判断的时候我们只要查找当前的 nums[i] ,以及挨着他的元素 nums[i]++ 在不在 HashSet 中,如果在的话就给结果ans++,并且一路继续查找,否则就重新开始。
判断在不在的过程交给 contains() 方法来完成。
class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> set=new HashSet<>();
for(int num: nums){
set.add(num);
}
int ans=0;
int count=0;
for(int i=0;i<nums.length;i++){
int num=nums[i];
while(set.contains(num)){
count++;
num++;
}
ans=Math.max(ans,count);
count=0;
}
return ans;
}
}
这种方法显然是可以提交通过的(虽然时间复杂度达不到要求),但是重复的工作显然还是集中在了查找部分。
对于任何一个元素,都在 Set 里,去查找和他连续的数字的存在性,我们先不考虑查找过程的时间复杂度。
假设数组是 [1,2,3,4,5],对于1,已经查找过一次,计算出连续长度为 5 ,之后对于 2,3,4,5 又进行了重复的操作,这样最坏情况下时间复杂度总共就达到了内外的O(n2)。
事实上,对于每一个连续序列而言,我们只希望判断一次,也就是从这个子序列的最小位置开始。那么**对于每一个元素 num ,先判断 num-1 是否存在,**就可以避免这种重复了。
修改后的代码如下:
for(int i=0;i<nums.length;i++){
int num=nums[i];
if(!set.contains(num-1)){
while(set.contains(num)){
count++;
num++;
}
ans=Math.max(ans,count);
count=0;
}
}
也就是只多加一行 if(!set.contains(num-1)){
这时候的时间复杂度又是多少呢?
- 首先用 O(n) 时间把数组存入set;
- 遍历数组,每一个元素,只有不存在 num-1 ,才会进入计数阶段;这些处于各个连续序列开始位置的元素,都会进入一次,然后进行计数,总共正好组成了原数组的长度。而其他的元素在判断 num-1 存在之后不会进入计数的循环。因此时间复杂度还是 O(n) 。
所以总时间复杂度是O(n)。原因得益于HashMap本身的查找方法时间复杂度是O(1),个人认为也不是严格意义的O(1),可以看看这篇博客的源码分析:
HashMap底层源码分析