最长连续序列问题解析
题目描述
给定一个未排序的整数数组 nums,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
要求设计并实现时间复杂度为 O(n) 的算法解决此问题。
示例
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9
解题思路
这道题可以有多种解法,我们将从简单到复杂,逐步优化我们的解决方案。
解法一:排序法(不符合题目要求)
这是一个直观但不符合题目 O(n) 时间复杂度要求的解法。
思路
- 先将数组排序
- 遍历排序后的数组,统计连续的数字序列长度
- 返回最长的序列长度
代码实现
var longestConsecutive = function(nums) {
if (nums.length === 0) return 0;
nums.sort((a, b) => a - b);
let maxLength = 1;
let currentLength = 1;
for (let i = 1; i < nums.length; i++) {
if (nums[i] !== nums[i-1]) {
if (nums[i] === nums[i-1] + 1) {
currentLength++;
} else {
maxLength = Math.max(maxLength, currentLength);
currentLength = 1;
}
}
}
return Math.max(maxLength, currentLength);
};
复杂度分析
- 时间复杂度:O(nlogn),主要是排序的时间复杂度
- 空间复杂度:O(1) 或 O(n),取决于排序算法的实现
优点和缺点
- 优点:直观,易于理解
- 缺点:不符合题目要求的 O(n) 时间复杂度
解法二:哈希表法(最优解)
这是一个符合题目要求的 O(n) 时间复杂度解法。
思路
- 使用 Set 存储所有数字,便于 O(1) 时间复杂度的查找
- 遍历数组,对每个数字 x,检查 x-1 是否存在:
- 如果 x-1 不存在,说明 x 可能是一个连续序列的起点
- 如果 x-1 存在,则跳过(因为我们会从更小的数字开始统计)
- 从每个可能的起点开始,统计连续序列的长度
- 维护最大长度并返回
代码实现
var longestConsecutive = function(nums) {
const numSet = new Set(nums);
let maxLength = 0;
for (const num of nums) {
if (!numSet.has(num - 1)) {
let currentNum = num;
let currentLength = 1;
while (numSet.has(currentNum + 1)) {
currentNum++;
currentLength++;
}
maxLength = Math.max(maxLength, currentLength);
}
}
return maxLength;
};
复杂度分析
- 时间复杂度:O(n),虽然有嵌套循环,但每个数字最多被访问两次
- 空间复杂度:O(n),用于存储 Set
优点和缺点
- 优点:符合题目要求的 O(n) 时间复杂度,不改变原数组顺序
- 缺点:需要额外的 O(n) 空间
解法三:并查集(高级解法)
这是一个使用并查集(Disjoint Set Union)的高级解法。
思路
- 创建一个并查集数据结构
- 遍历数组,将每个数字及其相邻数字(如果存在)合并到同一个集合
- 统计最大集合的大小,即为最长连续序列的长度
代码实现
class UnionFind {
constructor(n) {
this.parent = new Array(n).fill(0).map((_, i) => i);
this.size = new Array(n).fill(1);
}
find(x) {
if (this.parent[x] !== x) {
this.parent[x] = this.find(this.parent[x]);
}
return this.parent[x];
}
union(x, y) {
let rootX = this.find(x);
let rootY = this.find(y);
if (rootX !== rootY) {
this.parent[rootX] = rootY;
this.size[rootY] += this.size[rootX];
}
}
}
var longestConsecutive = function(nums) {
if (nums.length === 0) return 0;
const n = nums.length;
const uf = new UnionFind(n);
const numToIndex = new Map();
for (let i = 0; i < n; i++) {
if (numToIndex.has(nums[i])) continue;
numToIndex.set(nums[i], i);
if (numToIndex.has(nums[i] - 1)) {
uf.union(i, numToIndex.get(nums[i] - 1));
}
if (numToIndex.has(nums[i] + 1)) {
uf.union(i, numToIndex.get(nums[i] + 1));
}
}
return Math.max(...uf.size);
};
复杂度分析
- 时间复杂度:O(n),并查集操作的平均时间复杂度接近 O(1)
- 空间复杂度:O(n),用于存储并查集和 Map
优点和缺点
- 优点:高效,可以处理动态数据(如果需要支持添加新元素)
- 缺点:实现较为复杂,对于仅需要一次性计算的场景可能过于繁琐
总结
- 排序法:简单直观,但不符合题目的时间复杂度要求。
- 哈希表法:最优解,符合 O(n) 时间复杂度要求,实现相对简单。
- 并查集法:高级解法,适用于需要支持动态操作的场景。
对于这道题,推荐使用哈希表法,因为它既符合题目要求,又相对容易理解和实现。在实际编码中,我们应该根据具体需求和场景选择适当的解法。