LeetCode 128. 最长连续序列 —— 从排序到哈希集合

一、题目描述

给定一个未排序的整数数组 nums,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

要求 :设计并实现时间复杂度为 O(n) 的算法。

示例:

cpp 复制代码
输入:nums = [100, 4, 200, 1, 3, 2]
输出:4
解释:最长连续序列是 [1, 2, 3, 4],长度为 4。

输入:nums = [0, 3, 7, 2, 5, 8, 4, 6, 0, 1]
输出:9

输入:nums = [1, 0, 1, 2]
输出:3

数据范围:

0 <= nums.length <= 10^5

-10^9 <= nums[i] <= 10^9

二、思路演进

这道题看似简单,但由于数据范围极大(10^9)且要求 O(n) 时间,很容易踩坑。下面记录三次尝试的演进过程。

尝试一:排序 + 相邻比较

直觉思路:先排序,然后遍历判断相邻元素是否差 1。

cpp 复制代码
class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        int ans = 0, num = 1;
        for (int i = 0; i < nums.size() - 1; i++) {
            if (nums[i+1] - nums[i] == 1) {
                num++;
                ans = max(ans, num);
            } else {
                num = 1;
            }
        }
        return ans;
    }
};

存在的问题:

  1. 时间复杂度不达标 :排序需要 O(n log n),题目明确要求 O(n)。

  2. 重复元素处理错误 :如 [1, 1, 2],nums[i+1] - nums[i] == 0 会走 else 分支,导致 num 被错误重置。

  3. 边界问题 :空数组时 nums.size() - 1 在 size_t 下溢;单元素数组返回 0(正确应为 1)。

结论:思路直观但不满足要求,需要换方向。

尝试二:计数数组(桶思想)

直觉思路:用数组下标表示数字本身,值表示是否出现。然后扫描数组,统计连续非零段长度。

cpp 复制代码
class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        vector<int> a(10000, 0);
        for (int i = 0; i < nums.size(); i++) {
            a[nums[i]]++;
        }
        // ... 扫描 a,找最长连续非零段
    }
};

存在的问题:

  1. 数组大小硬编码错误 :nums[i] 范围是 -10^9 ~ 10^9,开 10000 数组不仅会越界,还会漏掉负数和超大数。

  2. sort 冗余 :使用计数数组后不需要排序,白白增加 O(n log n) 开销。

  3. flag 与 num 逻辑交叉混乱 :多个 if 分支配合 flag = -100 的初始值,有大量死代码(flag == -1 永不触发)。

进步点:已经意识到"用存在性代替相邻比较"可以规避重复元素问题,思路方向是对的。

结论:受限于数据范围,计数数组不可行,但"存在性查询"的想法需要保留,用哈希集合替代。

尝试三:哈希集合(正解)

核心观察:

对于任意一个连续序列 [x, x+1, x+2, ..., x+k],如果只从序列起点 (即 x-1 不存在于数组中)出发,向后逐一判断 x+1, x+2, ... 是否存在,就能得到该序列的长度。

这样做的好处是:每个数字最多只会被访问一次------因为它要么是某个序列的起点,要么是被某个起点"顺带数过去"的,不会重复遍历。

算法步骤:

  1. 将 nums 全部放入 unordered_set,实现 O(1) 存在性查询并自动去重。

  2. 遍历集合中每个数字 cur:

    • 若 cur - 1 存在 ,则 cur 不是起点,跳过。

    • 若 cur - 1 不存在 ,则 cur 是起点,向后不断查找 cur + 1, cur + 2, ...,统计当前序列长度。

  3. 更新全局最大值 ans。

代码实现:

cpp 复制代码
class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        unordered_set<int> num_set;
        for (int i = 0; i < nums.size(); i++) {
            num_set.insert(nums[i]);
        }

        int ans = 0;

        for (unordered_set<int>::iterator it = num_set.begin(); it != num_set.end(); it++) {
            int cur = *it;

            // 只从序列起点开始统计
            if (num_set.find(cur - 1) == num_set.end()) {
                int currentNum = cur;
                int num = 1;

                while (num_set.find(currentNum + 1) != num_set.end()) {
                    currentNum++;
                    num++;
                }

                if (num > ans) {
                    ans = num;
                }
            }
        }

        return ans;
    }
};

三、复杂度分析

维度 分析
时间复杂度 O(n)。虽然嵌套了 while,但每个数字只会在"作为某个起点开始的延伸"中被访问一次,总访问次数 O(n)。
空间复杂度 O(n)。哈希集合存储最多 n 个数字。

四、边界情况

输入 输出 说明
[] 0 集合为空,循环不执行,返回 0
[5] 1 5-1=4 不在集合,起点为 5,长度为 1
[1,1,2] 2 集合自动去重为 {1,2},最长连续为 2
[-10^9, -10^9+1] 2 哈希集合无范围限制,负数同样适用

五、方法对比总结

方法 时间复杂度 空间复杂度 是否满足题目 主要缺陷
排序 + 相邻比较 O(n log n) O(1) ❌ 重复元素处理错误
计数数组 O(n + V) O(V) ❌ 数据范围过大,无法开数组
哈希集合(正解) O(n) O(n) ✅ ---
相关推荐
多弗朗皮卡丘1 小时前
C++多继承
开发语言·c++·多继承
mmmmath_31 小时前
二叉树の一些知识
数据结构·算法
DongQiShanRen1 小时前
裁决台账双向互校(下):台账哈希链、三向对账与最小落地
人工智能·深度学习·算法·目标跟踪·自然语言处理·rust·哈希算法
程序员清风1 小时前
数据类型转换与日期时间处理
数据结构·pandas
SWAGGY..1 小时前
【C++进阶】:(7)红黑树的原理与 C++ 实现:结构设计、插入调整及性质验证
android·java·开发语言·c++·算法
-dzk-2 小时前
【技巧】LC 31.下一个排列
算法
(Charon)2 小时前
【C++面试】堆内存与栈内存:string、vector和对象到底存在哪里
开发语言·c++·面试
xlq223222 小时前
全面复习3
c++
(Charon)2 小时前
【C++面试】迭代器失效是什么?vector、list、map中如何避免迭代器失效
c++·算法·面试