128. 最长连续序列
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
题目分析
看到本题找最长连续序列,每个人都会想到两种最朴素的解法之一:
- 先排序,从前往后找最长连续上升序列即可。该思路简单有效,但是复杂度已经至少有 O ( n l o g n ) O(nlogn) O(nlogn)。实现起来也比较简单,在此不讨论该解法。
- 遍历数组中的每个元素num,然后以num为起点,每次+1向后遍历num+1,num+2,num+3…,判断这些元素是否存在于数组中。假设找到的最大的连续存在的元素为num+x,那么这个连续序列的长度即为x+1。最后将每个num所开始序列长度取个最大值即可。
优化的点主要有两个:
(1)判断num+1,num+2,num+3…是否在数组中。上面的代码是用直接遍历的方式去查找的,时间复杂度为
O
(
n
)
O(n)
O(n),我们可以改为哈希表查找,时间复杂度为
O
(
1
)
O(1)
O(1)。
(2)遍历数组中每个元素num。逐一遍历每个元素会产生很多冗余工作,实际上我们无需一次针对每个元素num去判断num+1,num+2,num+3…是否在数组中。如果num-1已经在数组中的话,那么num-1肯定会进行相应的+1遍历,然后遍历到num,而且从num-1开始的+1遍历必定比从num开始的+1遍历得到的序列长度更长。因此,我们便可将在一个连续序列中的元素进行删减,让其只在最小的元素才开始+1遍历。
比如,现有元素[1,2,4,3,5],当2,3,4,5发现均有比自己小1的元素存在,那么它们就不会开始+1遍历,而1是连续序列中最小的元素,没有比自己小1的元素存在,所以会开始+1遍历。 通过上述方式便可将时间复杂度优化至O(n)。
class Solution {
public int longestConsecutive(int[] nums) {
// 建立一个存储所有数的哈希表,同时起到去重功能
Set<Integer> set = new HashSet<>();
for (int num : nums) {
set.add(num);
}
int ans = 0;
// 遍历去重后的所有数字
for (int num : set) {
int cur = num;
// 只有当num-1不存在时,才开始向后遍历num+1,num+2,num+3......
if (!set.contains(cur - 1)) {
while (set.contains(cur + 1)) {
cur++;
}
}
// [num, cur]之间是连续的,数字有cur - num + 1个
ans = Math.max(ans, cur - num + 1);
}
return ans;
}
}
注意:上述代码虽然有两层循环for+while,但是由于if (!set.contains(cur - 1))判断的存在,每个元素只会被遍历一次,因此时间复杂度也为O(n)。
上面是一种朴素思路的优化方法,下面再介绍三种算法,分别为:
- 使用哈希表记录当前num能到达的连续最右边界;
其实方法1相比上面的更容易想到吧
class Solution {
public int longestConsecutive(int[] nums) {
// key表示num,value表示num最远到达的连续右边界
Map<Integer, Integer> map = new HashMap<>();
// 初始化每个num的右边界为自己
for (int num : nums) {
map.put(num, num);
}
int ans = 0;
for (int num : nums) {
//if (!map.containsKey(num - 1)) {
int right = map.get(num);
// 遍历得到最远的右边界
while (map.containsKey(right + 1)) {
right = map.get(right + 1);
}
// 更新右边界
map.put(num, right);
// 更新答案
ans = Math.max(ans, right - num + 1);
// }
}
return ans;
}
}
-
使用哈希表记录当前值所在的连续最大区间(动态规划)
用哈希表存储每个端点值对应连续区间的长度
若数已在哈希表中:跳过不做处理
若是新数加入:- 取出其左右相邻数已有的连续区间长度 left 和 right
- 计算当前数的区间长度为:cur_length = left + right+ 1
- 根据 cur_length 更新最大长度 max_length 的值 更新区间两端点的长度值
class Solution(object):
def longestConsecutive(self, nums):
hash_dict = dict()
max_length = 0
for num in nums:
if num not in hash_dict:
left = hash_dict.get(num - 1, 0)
right = hash_dict.get(num + 1, 0)
cur_length = 1 + left + right
if cur_length > max_length:
max_length = cur_length
hash_dict[num] = cur_length
hash_dict[num - left] = cur_length
hash_dict[num + right] = cur_length
return max_length
- 并查集
并查集实际上也是来记录右边界的,所有在一个连续区间内的元素都会在一个连通分量中,且这些元素的根结点都为最远的右边界元素。
具体思路是:
遍历所有元素num,如果num+1存在,将num加入到num+1所在的连通分量中;
重新遍历一遍所有元素num,通过find函数找到num所在分量的根结点,也就是最远右边界,从而求得连续区间的长度。