LeetCode 128. 最长连续序列

题目描述

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

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

最初思路

最开始的整体方向是:

  1. HashSet 对原数组去重。
  2. 再准备一张 HashMap,遍历集合时检查 x-1x+1 是否已经出现。
  3. 每发现一个相邻数字,就把全局计数器 cnt 加一。

去重和「看相邻数字」这个方向是对的。真正要求的是最长那一段的长度,而不是数组里所有相邻关系的总和。

问题出在哪里

第一版用一个贯穿整个循环的 cnt,每次看到相邻关系就 cnt++。以 [1, 2, 10, 11] 为例,两段长度都是 2,但 cnt 会把两段的相邻关系加在一起,得到的不是最长段。

中间又改成「找到起点后把 cnt 重置为 1,再检查一次 x+1」。这会带来两个新问题:

  1. HashSet 的遍历顺序不是从小到大。遇到新起点时直接 cnt = 1,前面已经数过的更长段会被清掉。例如 [1, 2, 6, 7, 8],如果先扫完 6,7,8 得到 3,再遇到起点 1,最终留下的可能是最后扫到的那一段,而不是最长的 3。
  2. 每一轮只检查旁边的 x+1 一次。长度大于 2 的段,不能靠「当前这个 x 再往后看一格」数完;而且 HashSet 无序,也不能指望 x=7 时再把 8 补进去。

另外,起点那一轮如果先 cnt = 1cnt++,紧接着又因为 x+1 存在再加一次,[1,2,3,4] 会把起点 1 重复计入。

正确思路

把数字放入 HashSet 去重后,遍历集合中的每个 x

  1. 如果 x-1 也在集合里,说明 x 不是这段的起点,直接跳过。
  2. 否则从 x 开始,令 y = x + 1,不断向后走,直到 y 不在集合中。
  3. 当前段长度是 y - x,用 ans 取历史最大值。

空数组时集合为空,循环不执行,ans 保持 0,不必单独特判。

核心洞察

一段连续序列只有一个真正的起点:集合里存在 x,但不存在 x-1

只从这样的 x 向后扫描,每段只会被数一次。如果对每个数字都向后 while,像 [1, 2, ..., n] 这种输入里,1 会扫 n 步,2 会扫 n-1 步,总时间会退化成 O(n²)

HashSet 遍历无序,所以不能靠「当前段最小值大于上一段最大值」来分段,也不能用一个全局 cnt 一路往上加。每段的长度要在这一段扫完时当场算出来,再和全局答案取最大。

手推过程

nums = [1, 2, 6, 7, 8] 为例。集合为 {1, 2, 6, 7, 8}ans 初始为 0。

复制代码
x = 1:1-1 = 0 不在集合中,是起点
      y 从 2 走到 3,3 不在集合中
      段长 = 3 - 1 = 2
      ans = max(0, 2) = 2

x = 2:2-1 = 1 在集合中,跳过

x = 6:6-1 = 5 不在集合中,是起点
      y 从 7 走到 9,9 不在集合中
      段长 = 9 - 6 = 3
      ans = max(2, 3) = 3

x = 7:7-1 = 6 在集合中,跳过
x = 8:8-1 = 7 在集合中,跳过

无论集合按什么顺序遍历,16 各自作为起点只会被完整扫描一次,最终 ans 仍是 3。

再看 [1, 2, 10, 11]:两段都是 2,ans 取最大值后仍为 2,不会把两段加在一起。

伪代码

下面的伪代码和 Java 实现一一对应:

复制代码
函数 longestConsecutive(nums):
    创建集合 m
    将 nums 中的每个数加入 m

    ans <- 0
    对于 m 中的每个 x:
        如果 m 中包含 x - 1:
            跳过
        y <- x + 1
        当 m 中包含 y:
            y <- y + 1
        ans <- max(ans, y - x)

    返回 ans

Java 代码

复制代码
class Solution {
    public int longestConsecutive(int[] nums) {
        Set<Integer> m = new HashSet<>();
        for (int x : nums) {
            m.add(x);
        }
        int ans = 0;
        for (int x : m) {
            if (m.contains(x - 1))
                continue;
            int y = x + 1;
            while (m.contains(y)) {
                y++;
            }
            ans = Math.max(ans, y - x);
        }
        return ans;
    }
}

易错点

  1. 要统计的是最长那一段,不是所有相邻关系的总和。全局 cnt 一路累加会把多段拼在一起。
  2. 遇到新起点时不能只重置当前计数,必须用 Math.max 保留已经出现过的更长段。
  3. HashSet 遍历无序,不能依赖「上一段」或「当前段最小值大于上一段最大值」来切分。
  4. 只检查一次 x+1 数不完长度大于 2 的段;要从起点用 while 一直走到断掉。
  5. 起点那一轮不要把 x 加两次。用 y - x 计算段长,可以避开手动 cnt++ 的重复计数。
  6. 必须先判断 x-1 不存在再向后扫描,否则复杂度会退化。

建议测试用例

复制代码
nums = []
期望:0

nums = [1]
期望:1

nums = [1, 2, 10, 11]
期望:2
说明:两段互不相邻,答案是最长段,不是两段之和

nums = [1, 2, 6, 7, 8]
期望:3
说明:无论先扫到哪一段,最终都要留下更长的那一段

nums = [100, 4, 200, 1, 3, 2]
期望:4

nums = [1, 2, 0, 1]
期望:3
说明:HashSet 去重后是 0,1,2

复杂度分析

设去重后的数字个数为 n

每个数字进入 HashSet 一次。外层遍历每个数字时,若它不是起点则直接跳过;while 只从起点往后走,每个数字最多作为 y 被检查一次。因此:

  • 时间复杂度:O(n)
  • 空间复杂度:O(n),用于保存 HashSet

如果去掉「x-1 不存在才扫描」这个判断,对 [1, 2, ..., n] 会变成 O(n²)

总结

这道题的关键不是把相邻数字都数一遍,而是先认出每一段的起点。x-1 不在集合中时,从 x 向后走到断掉,用 y - x 得到段长,再和全局答案取最大。

本次主要问题有两层:一开始把多段长度加在了一起;后来虽然找到了起点,但既没有保留历史最长,也没有从起点把整段扫完。遇到类似「无序集合里找连续段」的问题时,可以先问自己:当前这个数是不是这段的起点?

相关推荐
linx2952 小时前
单元四 · 对称认知·上:内存与指针
c语言·开发语言·数据结构·嵌入式硬件·算法
-dzk-2 小时前
【二叉树】LC 236.二叉树的最近公共祖先
数据结构·二叉树
午彦琳2 小时前
2026.9.11
数据结构·python·算法
careathers3 小时前
【数据结构】链表
数据结构·链表
302wanger3 小时前
蛋炒饭周刊 · 第 1 期(2026-09-07至2026-09-11)
算法
shehuiyuelaiyuehao4 小时前
算法43,外观数列,模拟算法+双指针
java·算法
alphaTao4 小时前
LeetCode 每日一题 2026/9/7-2026/9/13
算法·leetcode
kanhaoning4 小时前
Agent 记忆出现重复和矛盾怎么优化:我实测了6种去重和更新策略,找到了低成本维护记忆质量的方法
算法