题目
Given an unsorted array of integers, find the length of the longest consecutive elements sequence.
Your algorithm should run in O(n) complexity.
Example:
Input: [100, 4, 200, 1, 3, 2] Output: 4 Explanation: The longest consecutive elements sequence is [1, 2, 3, 4]. Therefore its length is 4.
分析
这道题让我们求出一个无序数组中最长的连续序列的长度,难度在于时间复杂度要求在O(n)内,用并查集可以达到这个要求。
首先来复习一下并查集这个数据结构。
并查集
并查集是一种树型数据结构,用于处理一些不相交集合的合并及查询问题,用它来判断元素所在集合所用时间接近常数级,Kruskal最小生成树算法就用到了并查集来实现。
假设有一个动态集合S={s1,s2,s3,…sn},每个子集合通过一个代表来标识,代表就是这个子集合中的一个元素,即根元素。要判断两个元素是否属于同一个子集合,判断它们的根元素是否是同一个代表即可。
随着union操作,子集合的个数会越来越少,因此称之为动态集合。
并查集主要涉及3种基本操作:
makeset
:初始化,每个元素作为一个子集合,它的父节点是本身,集合树高度为1find(x)
:找出某个元素x
的根元素,所需时间与树的高度成正比。在优化的并查集中,经常在find(x)
过程中把根节点更新为x
的父节点,这样可以降低树的高度,在总体上提高find
和union
的效率,这个优化被称为路径压缩union(x,y)
:合并包含x
和y
的集合,一般是让较低的树的根指向较高的树的根。只有当两棵树高度相同时,合并后总高度才增加。
解法一
在本题中,连续的序列可以视为一个集合,集合中的元素值是连续的,而我们要做的事情就是找到最大的集合大小。如何判断两个集合需要合并呢?对于x,x和x+1是连续的,它们属于同一个序列,所以要合并x和x+1所在的子集合。对于x-1同理。
使用并查集,我们可以在O(n)时间内完成任务,因为经过优化后的find
操作耗时接近常数级。
本题,我们可以遍历一次数组,假设当前遍历到的元素值为x
,查询x+1
和x-1
是否在数组中,如果在,就合并它们所在的子集合,最终找出最大子集合的大小。
通过使用hash map
,我们可以解决两个问题:
- 并查集中不能有两个重复的元素,否则结果会偏大
- 查询
x+1
和x-1
是否在数组中的操作耗时O(1)
另外需要注意,在这个问题的并查集中使用级别来合并子集合并不适用,要换成根节点所具有的孩子数。
代码
class UnionSet {
private:
unordered_map<int, int> parent;
unordered_map<int, int> count; // count[x]:以x为root的元素个数
public:
int max_count;
UnionSet(vector<int>& nums) {
max_count = 0;
for (auto i : nums) {
parent[i] = i;
count[i] = 1;
max_count = 1;
}
}
bool exist(int x) {
return parent.find(x) != parent.end();
}
int find(int x) {
if (parent[x] == x) return x;
else {
parent[x] = find(parent[x]);
return parent[x];
}
}
void union_set(int x1, int x2) {
int root1 = find(x1), root2 = find(x2);
if (root1 != root2) {
parent[root1] = root2;
count[root2] += count[root1];
max_count = max(max_count, count[root2]);
}
}
};
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
UnionSet set(nums);
for (auto i : nums) {
if (set.exist(i - 1)) set.union_set(i, i - 1);
if (set.exist(i + 1)) set.union_set(i, i + 1);
}
return set.max_count;
}
};
解法二
首先思考暴力解法,枚举数组中的每一个元素x,判断x+1、x+2、…、x+y是否在数组中,即寻找从x开始的最长连续序列。假如我们将数组转换成哈希表的形式来判断元素是否存在,最坏情况下也需要
O
(
n
2
)
O(n^2)
O(n2)的时间复杂度:假设存在x,x+1,…,x+y的连续序列,依次枚举x、x+1、…x+y,则需要判断的次数是y+1 + y+ … + 1 = y^2 / 2 + y。
如何优化这个暴力解法?当我们枚举完x时,我们已经找到了从x开始的最长连续序列,也就是说,在枚举到x+1、…、x+y时,都不需要进行判断,之后(或之前)在枚举到x时会给它们进行判断的。所以,当我们枚举到x时,可以先判断x-1是否在数组中,如果不在,那么x就可以作为一段连续序列的开头,开始进行判断。
代码
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> m;
int result = 0;
for (auto i : nums) m.insert(i);
for (auto i : nums) {
if (m.find(i - 1) == m.end()) {
int j = i + 1;
while (m.find(j) != m.end()) j++;
result = max(result, j - i);
}
}
return result;
}
};
复杂度分析
虽然开起来是两层循环,但并不是每一次都会进入内层循环的,总共在内层循环的次数最多是 n n n,所以时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)。