本文已参与「新人创作礼」活动,一起开启掘金创作之路。
题目
方法一:哈希表
暴力匹配的思路是枚举数组中的每一个数,以该数为起点,不断尝试匹配、、是否存在,对于匹配的过程,可以用的复杂度去暴力遍历数组,更高效的方法可以用哈希表提前存储好数组中的数,这样可以用复杂度来查找一个数是否存在,即使这样整个算法的复杂度最坏情况下仍然为,外层需枚举个数,内层也需暴力匹配次。
上述过程其实做了很多重复工作,假如一直有一个、、、的连续序列,那么就没有必要再以或者等等开始枚举了,得到的结果肯定不会优于以为起点的答案,因此外层每次枚举时利用哈希表判断是否存在,若存在则跳过此数。
增加了判断跳过的逻辑后,时间复杂度为,外层循环需要的时间复杂度,但是数组中的每个数只会进入内层循环一次。
class Solution {
public int longestConsecutive(int[] nums) {
int n = nums.length;
Set<Integer> set = new HashSet<>();
for (int num : nums) {
set.add(num);
}
int res = 0;
for (int i = 0; i < n; i++) {
if (!set.contains(nums[i] - 1)) {
int curNum = nums[i];
int cur = 1;
while (set.contains(curNum + 1)) {
curNum += 1;
cur++;
}
res = Math.max(res, cur);
}
}
return res;
}
}
- 时间复杂度:,为数组的长度
- 空间复杂度:。
方法二:并查集
所有在一个连续区间的元素都会在一个连通分量中,且该连通分量的根节点为该连续区间中的最大值:
- 遍历数组,如果存在,将加入所在的连通分量中
- 重新遍历数组,通过
find函数找到所在分量的根节点,也就是其所在连续区间的最大值,从而求出连续区间的长度
class UnionFind {
/**
* 记录每个节点的父节点
*/
private Map<Integer, Integer> parent;
public UnionFind(int[] nums) {
parent = new HashMap<>();
for (int num : nums) {
// 初始化父节点为自身
parent.put(num, num);
}
}
/**
* 寻找x的父节点,实际上也就是x的最远连续右边界,也就是其所在连续区间的最大值
* @param x
* @return
*/
public Integer find(int x) {
if (!parent.containsKey(x)) {
return null;
}
while (x != parent.get(x)) {
// 路径压缩
parent.put(x, parent.get(parent.get(x)));
x = parent.get(x);
}
return x;
}
/**
* 合并两个连通分量,在本题中只用来将num并入到num+1的连续区间中
* @param x
* @param y
*/
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return ;
}
// 将num所在树的根节点设置为num+1所在树的根节点
parent.put(rootX, rootY);
}
}
class Solution {
public int longestConsecutive(int[] nums) {
UnionFind uf = new UnionFind(nums);
int ans = 0;
for (int num : nums) {
// 当num+1存在,将num合并到num+1所在集合中
if (uf.find(num + 1) != null) {
uf.union(num, num + 1);
}
}
for (int num : nums) {
// 找到num的最远连续右边界,也就是其所在连续区间的最大值
int right = uf.find(num);
ans = Math.max(ans, right - num + 1);
}
return ans;
}
}
- 时间复杂度:,路径压缩后的并查集时间复杂度近似为
- 空间复杂度: