最长连续序列问题解析

227 阅读4分钟

最长连续序列问题解析

题目描述

给定一个未排序的整数数组 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) 时间复杂度要求的解法。

思路
  1. 先将数组排序
  2. 遍历排序后的数组,统计连续的数字序列长度
  3. 返回最长的序列长度
代码实现
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) 时间复杂度解法。

思路
  1. 使用 Set 存储所有数字,便于 O(1) 时间复杂度的查找
  2. 遍历数组,对每个数字 x,检查 x-1 是否存在:
    • 如果 x-1 不存在,说明 x 可能是一个连续序列的起点
    • 如果 x-1 存在,则跳过(因为我们会从更小的数字开始统计)
  3. 从每个可能的起点开始,统计连续序列的长度
  4. 维护最大长度并返回
代码实现
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)的高级解法。

思路
  1. 创建一个并查集数据结构
  2. 遍历数组,将每个数字及其相邻数字(如果存在)合并到同一个集合
  3. 统计最大集合的大小,即为最长连续序列的长度
代码实现
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
优点和缺点
  • 优点:高效,可以处理动态数据(如果需要支持添加新元素)
  • 缺点:实现较为复杂,对于仅需要一次性计算的场景可能过于繁琐

总结

  1. 排序法:简单直观,但不符合题目的时间复杂度要求。
  2. 哈希表法:最优解,符合 O(n) 时间复杂度要求,实现相对简单。
  3. 并查集法:高级解法,适用于需要支持动态操作的场景。

对于这道题,推荐使用哈希表法,因为它既符合题目要求,又相对容易理解和实现。在实际编码中,我们应该根据具体需求和场景选择适当的解法。