自己实现的LeetCode相关题解代码库:https://github.com/Yuri0314/Leetcode
给定一个未排序的整数数组,找出最长连续序列的长度。
要求算法的时间复杂度为 O(n)。
示例:
输入: [100, 4, 200, 1, 3, 2]
输出: 4
解释: 最长连续序列是 [1, 2, 3, 4]。它的长度为 4。
看到这道题我首先的思路是对数组先进行排序,然后操作就很简单了,虽然也有想到使用Hash的方法,但没有想出该如何应用。
看完题解之后才发现这是一道典型的应用Hash的O(1)查找效率的题目,下面我总结了使用Hash法和并查集实现的多种实现方法并依次进行说明。
*注:暴力法和排序法的实现及解释可参考官方题解:https://leetcode.com/problems/longest-consecutive-sequence/solution/
官方解法不用过多解释,基本步骤就是:
class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> num_set = new HashSet<Integer>();
for (int num : nums)
num_set.add(num);
int longestStrak = 0;
for (int num : num_set) {
if (!num_set.contains(num - 1)) {
int curNum = num;
int curStrak = 1;
while (num_set.contains(++curNum)) ++curStrak;
longestStrak = Math.max(longestStrak, curStrak);
}
}
return longestStrak;
}
}
该方法的核心思路在于,对遍历到的每一个数字,将其所在串的长度设为“比其值小1的数记录的串长度”+“比其值大1的数记录的串长度”+1,同时在更新数据过程中更新最大序列串长度值。
在该方法中,需要快速找到比当前数字小1或者大1的数字对应记录的串长度,因此使用了一个Hash表来记录。
class Solution {
public int longestConsecutive(int[] nums) {
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
int ans = 0;
for (int num : nums) {
if (!map.containsKey(num)) {
int leftLength = (map.containsKey(num - 1)) ? map.get(num - 1) : 0;
int rightLength = (map.containsKey(num + 1)) ? map.get(num + 1) : 0;
int curLength = leftLength + rightLength + 1;
map.put(num - leftLength, curLength);
map.put(num + rightLength, curLength);
map.put(num, curLength);
ans = Math.max(ans, curLength);
}
}
return ans;
}
}
由第一种Hash法,即官方给出的Hash解法我们可以想到,是否可以无需判断遍历到的当前数字是所在序列最小值这一步操作呢?其实是可以的,官方题解中进行这一步判断是因为如果这个数字不是所在序列串的最小值,那么它计算得到的所在序列长度一定不是最小的,也就是说,遍历到其他也在该序列上的数字计算得到的长度可能比它大。其关键在于,对在同一个序列上的数字,如果不加判断,可能会出现重复计算。
针对这个问题,可以在遍历每个数字时,分别向其变大变小方法都查找是否在集合中存在该数字,然后在查找计算长度的过程中动态删除掉该数字,使得一个序列串中的所有数字只会被计算一次。
class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> set = new HashSet<Integer>();
int ans = 0;
for (int num : nums) set.add(num);
for (int num : nums) {
int length = 1;
int cur = num;
while (set.contains(--cur)) {
++length;
set.remove(cur);
}
cur = num;
while (set.contains(++cur)) {
++length;
set.remove(cur);
}
set.remove(num);
ans = Math.max(ans, length);
}
return ans;
}
}
因为本题中要统计数组中数字能组成连续序列串值的最大长度,而每一个这样的串值很明显就是一个集合,可以通过不断归并查找的方式将数字加入对应的集合中。很明显,这可以使用并查集来解决。
在并查集UnionFind中我定义了两个Map,分别表示该数字到所在集合树根的映射和该数字到所在集合存储元素数的映射,同时使用maxCount变量记录了在元素不断归并入对应集合过程中的最大长度。
在使用时,遍历输入数组中的每个数字,不断调用union方法对当前数字和比其值大1的数字进行归并操作即可。
注意:为了达到题目要求的O(n)时间复杂度,必须在查找元素所在集合的根元素的过程中进行路径压缩操作。
class Solution {
public int longestConsecutive(int[] nums) {
UnionFind uf = new UnionFind(nums);
for (int num : nums) {
uf.union(num, num + 1);
}
return uf.getMaxCount();
}
class UnionFind {
private Map<Integer, Integer> unionSet = new HashMap<Integer, Integer>();
private Map<Integer, Integer> countSet = new HashMap<Integer, Integer>();
private int maxCount;
public UnionFind(int[] nums) {
for (int num : nums) {
unionSet.put(num, num);
countSet.put(num, 1);
}
maxCount = nums.length > 0 ? 1 : 0;
}
public int getRoot(int num) {
if (num == unionSet.get(num))
return num;
else {
unionSet.put(num, getRoot(unionSet.get(num)));
return unionSet.get(num);
}
}
public void union(int a, int b) {
if (!unionSet.containsKey(a) || !unionSet.containsKey(b)) return;
int rootA = getRoot(a);
int rootB = getRoot(b);
if (rootA == rootB) return;
int newCount = countSet.get(rootA) + countSet.get(rootB);
if (countSet.get(rootA) < countSet.get(rootB)) {
unionSet.put(rootA, rootB);
countSet.put(rootB, newCount);
}
else {
unionSet.put(rootB, rootA);
countSet.put(rootA, newCount);
}
maxCount = Math.max(maxCount, newCount);
}
public int getMaxCount() {
return this.maxCount;
}
}
}
与上一种并查集方法类似,其基本思路一致,不同之处在于该方法中的UnionFind并查集类使用了两个数组来实现,分别表示数字对应集合树根索引与数字对应集合存储元素数,而由数字值到对应索引位置的这一映射使用一个Map来记录。
注意:这种实现方式下,必须对每个遍历的数字从双向进行union尝试,即union比当前数字大1和小1的数;否则,如果只尝试union大1的数,当数组中的元素为[1, 2, 3, 4]这样某个串中的大数出现在小数前时,将会得到错误答案。
class Solution {
public int longestConsecutive(int[] nums) {
UnionFind uf = new UnionFind(nums.length);
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; ++i) {
if (map.containsKey(nums[i])) continue;
map.put(nums[i], i);
if (map.containsKey(nums[i] + 1))
uf.union(i, map.get(nums[i] + 1));
if (map.containsKey(nums[i] - 1))
uf.union(i, map.get(nums[i] - 1));
}
return uf.getMaxCount();
}
class UnionFind {
private int[] unionSet;
private int[] countSet;
private int maxCount;
public UnionFind(int length) {
unionSet = new int[length];
countSet = new int[length];
for (int i = 0; i < length; ++i) {
unionSet[i] = i;
countSet[i] = 1;
}
maxCount = length > 0 ? 1 : 0;
}
public int getRoot(int i) {
while (i != unionSet[i]) {
unionSet[i] = unionSet[unionSet[i]];
i = unionSet[i];
}
return i;
}
public void union(int a, int b) {
int rootA = getRoot(a);
int rootB = getRoot(b);
if (rootA == rootB) return;
if (countSet[rootA] < countSet[rootB]) {
unionSet[rootA] = unionSet[rootB];
countSet[rootB] += countSet[rootA];
maxCount = Math.max(maxCount, countSet[rootB]);
}
else {
unionSet[rootB] = unionSet[rootA];
countSet[rootA] += countSet[rootB];
maxCount = Math.max(maxCount, countSet[rootA]);
}
}
public int getMaxCount() {
return this.maxCount;
}
}
}
上述所有方法的均摊时间复杂度均为O(n),空间复杂度为O(n)