【码道初阶-Hot100】LeetCode 128. 最长连续序列:从排序双指针扫描到 HashSet,一文讲透为什么 O(n) 解法要用哈希

LeetCode 128. 最长连续序列:从排序双指针扫描到 HashSet,一文讲透为什么 O(n) 解法要用哈希

摘要

LeetCode 128. 最长连续序列(Longest Consecutive Sequence) 是一道非常经典的数组题。题目本身不难理解,但它真正的考点在于:

  • 如何识别"连续序列"不要求在原数组中相邻
  • 如何处理重复元素
  • 如何把常规排序解法优化到题目要求的 O(n)

很多人第一次做这道题时,都会先想到:

  • 先排序
  • 再线性扫描连续段
  • 统计最长长度

这个思路完全正确,而且很好理解,但时间复杂度是 O(n log n)不满足题目明确要求的 O(n)

这篇文章会系统梳理两种做法:

  1. 排序 + 线性扫描:容易理解,适合建立题感
  2. HashSet 去重 + 只从序列起点出发扩展 :满足题目要求的 O(n) 标准解法

文章会重点讲清楚:

  • 为什么排序解法能做对,但不达标
  • 为什么哈希解法可以做到 O(n)
  • 为什么哈希解法必须"只从序列起点开始扩展"
  • 为什么这样不会漏掉任何答案,也不会重复计算

目录

文章目录


一、题目描述

给定一个未排序的整数数组 nums,找出数字连续的最长序列的长度。

注意:

这里的"连续"只要求数值连续,不要求这些元素在原数组中的下标连续。

示例 1:

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

示例 2:

java 复制代码
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9

示例 3:

java 复制代码
输入:nums = [1,0,1,2]
输出:3

二、这道题真正的难点是什么

这道题表面上看是在求"最长连续段",但真正难点主要有三个:

1. 连续的是"数值",不是"下标"

例如:

java 复制代码
[100,4,200,1,3,2]

虽然 1,2,3,4 在原数组里并不挨着,但它们仍然构成一个长度为 4 的连续序列。

2. 数组里可能有重复元素

例如:

java 复制代码
[1,0,1,2]

这里 1 出现了两次,计算连续序列时不能被重复计数。

3. 题目要求时间复杂度为 O(n)

这意味着:

  • 排序解法虽然能做对
  • 但因为排序是 O(n log n),严格来说不符合题意要求

所以真正的重点在于:如何不用排序,直接在线性时间内找到最长连续序列。


三、先从最自然的思路开始:排序 + 线性扫描

很多人第一次想到的做法是:

  1. 先对数组排序
  2. 排序后,连续的数字会靠在一起
  3. 线性扫描数组,统计每一段连续序列的长度
  4. 遇到重复数字时跳过
  5. 最后取最大值

这个思路非常自然,而且代码也不难写。


四、排序解法的核心逻辑

假设数组排序后变成:

java 复制代码
[1,2,3,4,100,200]

那么就可以线性扫描:

  • 1 -> 2,连续,长度加 1
  • 2 -> 3,连续,长度加 1
  • 3 -> 4,连续,长度加 1
  • 4 -> 100,断开,结束这一段

如果有重复元素,比如:

java 复制代码
[0,1,1,2]

那么:

  • 0 -> 1,连续
  • 1 -> 1,重复,不增加长度,但继续往后看
  • 1 -> 2,连续

所以重复元素要跳过但不打断当前连续段


五、排序 + 线性扫描代码(高质量注释版)

下面是整理后的排序解法,并加上详细注释。

java 复制代码
import java.util.*;

class Solution {
    public int longestConsecutive(int[] nums) {
        // 先排序
        Arrays.sort(nums);

        int n = nums.length;

        // 记录最长连续序列长度
        int res = 0;

        // i 用来表示当前连续段的起点
        for (int i = 0; i < n; ) {
            // 当前连续段至少包含 nums[i] 自己
            int count = 1;

            // j 用来向后扫描,寻找这一段连续区间的终点
            int j = i + 1;

            while (j < n) {
                // 如果当前数字和前一个数字正好相差 1
                // 说明连续段可以继续扩展
                if (nums[j] - nums[j - 1] == 1) {
                    count++;
                    j++;
                }
                // 如果当前数字和前一个数字相同
                // 说明是重复元素,不增加长度,但也不打断连续段
                else if (nums[j] == nums[j - 1]) {
                    j++;
                }
                // 否则说明连续段已经断开
                else {
                    break;
                }
            }

            // 更新最长长度
            res = Math.max(res, count);

            // 直接跳到下一段的起点
            i = j;
        }

        return res;
    }
}

六、这段排序代码到底在做什么

这段代码的本质可以概括为一句话:

先把无序问题变成有序问题,再按段扫描连续区间。

其中:

  • i 表示当前这一段连续序列的起点
  • j 负责一直向后找,直到这一段不能再延伸
  • count 记录当前连续段长度
  • res 维护全局最大值

七、为什么这段代码能正确处理重复元素

这是排序解法中最关键的细节之一。

例如数组:

java 复制代码
[1,0,1,2]

排序后:

java 复制代码
[0,1,1,2]

如果扫描到:

  • 0 -> 1,连续,count = 2
  • 1 -> 1,重复,这时不能把 count 加 1,因为重复元素不应该重复计入序列长度
  • 但也不能直接 break,因为后面的 2 仍然可能和当前连续段连上

所以这里正确的做法是:

java 复制代码
else if (nums[j] == nums[j - 1]) {
    j++;
}

也就是:

重复元素跳过,但不打断当前连续段。

这点非常重要。


八、排序解法的优点与缺点

优点

  • 思路直观
  • 代码容易写
  • 容易理解和验证正确性
  • 适合第一次建立题感

缺点

  • 排序时间复杂度是 O(n log n)
  • 不满足题目明确要求的 O(n)

这意味着:

这是一种"能做对,但不是题目最优要求"的解法。


九、严格来说,这个做法算不算"双指针"

很多人会把这类写法称为"双指针",因为这里确实用了两个下标 ij

但更准确地说,它其实更接近:

排序 + 分段扫描

因为:

  • 它不是经典的左右夹逼双指针
  • 也不是在原数组上做双端收缩
  • 它的本质是在排序后,用两个下标扫描一段连续区间

所以在写博客或面试表述时,更推荐叫它:

排序 + 线性扫描

或者
排序后用两个下标维护连续段

这样更准确。


十、为什么这道题可以用哈希做到 O(n)

接下来进入这道题真正的标准解法。

题目要求 O(n),这意味着不能排序。

那么就必须想办法:

  • 在不排序的情况下
  • 快速判断某个数在不在数组里
  • 快速扩展连续序列

这正是 HashSet 最擅长的事情。

因为 HashSet 支持平均 O(1) 的查找。


十一、哈希解法的核心思路

思路分三步:

第一步:把所有数字放进 HashSet

这样就能在平均 O(1) 时间内判断:

java 复制代码
x 是否存在
x + 1 是否存在
x - 1 是否存在

第二步:只从"连续序列的起点"开始扩展

什么叫起点?

如果某个数 x 的前一个数 x - 1 不存在,那么说明它是一个连续序列的起点。

例如:

java 复制代码
[100,4,200,1,3,2]

放入集合后:

  • 1 没有前驱 0,所以 1 是起点
  • 2 有前驱 1,不是起点
  • 3 有前驱 2,不是起点
  • 4 有前驱 3,不是起点
  • 100 没有前驱 99,是起点
  • 200 没有前驱 199,是起点

第三步:从起点不断向后扩展

如果当前是起点 x,就不断看:

java 复制代码
x + 1 是否存在
x + 2 是否存在
x + 3 是否存在
...

直到不存在为止,这样就能得到这一段连续序列的长度。


十二、为什么一定要"只从起点出发"

这是哈希解法最关键的优化点。

假设数组中有连续序列:

java 复制代码
[1,2,3,4,5]

如果不区分起点,而是对每个数都向后扩展:

  • 1 扩展一遍:长度 5
  • 2 扩展一遍:长度 4
  • 3 扩展一遍:长度 3
  • 4 扩展一遍:长度 2
  • 5 扩展一遍:长度 1

这样就会重复计算很多次,复杂度退化。

而如果只从起点 1 出发:

  • 一次就能得到整个序列长度 5

其余 2,3,4,5 因为都有前驱,所以全部跳过。

这就是哈希解法能做到 O(n) 的核心原因。


十三、HashSet 标准解法代码(高质量注释版)

下面给出这道题最经典、最推荐掌握的写法。

java 复制代码
import java.util.*;

class Solution {
    public int longestConsecutive(int[] nums) {
        // 用 HashSet 去重,并支持 O(1) 平均时间查找
        Set<Integer> set = new HashSet<>();

        for (int num : nums) {
            set.add(num);
        }

        int res = 0;

        // 遍历集合中的每个数
        for (int num : set) {
            // 只有当 num - 1 不存在时,num 才是一个连续序列的起点
            // 只有起点才需要向后扩展
            if (!set.contains(num - 1)) {
                int cur = num;
                int count = 1;

                // 不断检查后继是否存在
                while (set.contains(cur + 1)) {
                    cur++;
                    count++;
                }

                // 更新最长长度
                res = Math.max(res, count);
            }
        }

        return res;
    }
}

十四、哈希代码逐步拆解

1. 先放入集合

java 复制代码
Set<Integer> set = new HashSet<>();
for (int num : nums) {
    set.add(num);
}

作用有两个:

  • 去重
  • 支持快速查找

例如数组:

java 复制代码
[1,0,1,2]

放入集合后就是:

java 复制代码
{0,1,2}

重复的 1 自动被消掉了。


2. 判断是否为起点

java 复制代码
if (!set.contains(num - 1))

这句的意思是:

如果 num - 1 不存在,那么 num 才是某个连续序列的起点。

例如:

  • 1 前面没有 0,那么 1 是起点
  • 2 前面有 1,那么 2 不是起点

3. 从起点向后扩展

java 复制代码
while (set.contains(cur + 1)) {
    cur++;
    count++;
}

只要后继存在,就说明连续序列还能继续延长。

例如从 1 开始:

  • 2,长度变 2
  • 3,长度变 3
  • 4,长度变 4
  • 没有 5,停止

4. 更新答案

java 复制代码
res = Math.max(res, count);

每找到一段完整连续序列,就更新一次全局最大值。


十五、用示例完整走一遍哈希过程

以:

java 复制代码
nums = [100,4,200,1,3,2]

为例。

第一步:放入集合

java 复制代码
set = {100,4,200,1,3,2}

第二步:遍历集合

遍历到 100
  • 99 不存在
  • 所以 100 是起点
  • 101 是否存在:不存在
  • 当前长度 = 1
遍历到 4
  • 3 存在
  • 所以 4 不是起点,跳过
遍历到 200
  • 199 不存在
  • 所以 200 是起点
  • 201 是否存在:不存在
  • 当前长度 = 1
遍历到 1
  • 0 不存在
  • 所以 1 是起点
  • 2:存在
  • 3:存在
  • 4:存在
  • 5:不存在
  • 当前长度 = 4

最终答案为 4


十六、为什么哈希解法的时间复杂度是 O(n)

很多人看到这里会问:

外层一个循环,内层还有一个 while,为什么不是 O(n^2)

关键点在于:

每个数只会在某一条连续序列扩展中被真正访问一次。

例如序列:

java 复制代码
[1,2,3,4,5]

只有从 1 这个起点开始,才会一路访问到 2,3,4,5

2,3,4,5 自己不会再作为起点去重复扩展。

所以整体来看,每个元素最多被:

  • 外层遍历一次
  • 内层扩展参与一次

总复杂度仍是平均 O(n)


十七、两种解法对比

1. 排序 + 线性扫描

思路:

  • 排序
  • 扫描连续段
  • 跳过重复值

时间复杂度:

java 复制代码
O(n log n)

空间复杂度:

java 复制代码
取决于排序实现,通常可视为 O(log n) 或 O(1) 额外空间

特点:

  • 易理解
  • 易写
  • 但不满足题目要求的 O(n)

2. HashSet 解法

思路:

  • 放入集合
  • 只从起点开始扩展
  • 统计每条连续序列长度

时间复杂度:

java 复制代码
平均 O(n)

空间复杂度:

java 复制代码
O(n)

特点:

  • 满足题目要求
  • 去重天然完成
  • 是标准最优解

十八、推荐的面试写法

如果是面试中回答这道题,建议这样组织表达:

先说排序解法

可以先说:

最直观的方法是先排序,再线性扫描连续段,遇到重复元素跳过,复杂度是 O(n log n)

这说明你对题目有基本掌握。

再指出不满足题意

接着说:

但题目要求 O(n),所以排序不能作为最终最优解。

这说明你看懂了题目要求。

最后给出哈希优化

然后说:

可以把所有数放进 HashSet,只从没有前驱的数开始向后扩展,这样每个连续序列只会被统计一次,总复杂度平均 O(n)

这就是完整且高质量的面试回答路径。


十九、推荐提交版代码

下面给出一版更适合正式提交和面试书写的标准写法。

java 复制代码
import java.util.*;

class Solution {
    public int longestConsecutive(int[] nums) {
        Set<Integer> set = new HashSet<>();
        for (int num : nums) {
            set.add(num);
        }

        int res = 0;

        for (int num : set) {
            // 只有没有前驱的数,才是连续序列的起点
            if (!set.contains(num - 1)) {
                int cur = num;
                int count = 1;

                while (set.contains(cur + 1)) {
                    cur++;
                    count++;
                }

                res = Math.max(res, count);
            }
        }

        return res;
    }
}

二十、面试高频追问总结

1. 为什么排序解法不满足题意

因为排序本身需要 O(n log n),而题目要求实现 O(n)

2. 为什么 HashSet 能解决这题

因为它能在平均 O(1) 时间判断某个数是否存在,非常适合做"连续性查询"。

3. 为什么只从起点开始扩展

为了避免对同一条连续序列重复统计,否则复杂度会退化。

4. 为什么重复元素不会影响结果

因为放进 HashSet 后,重复元素会自动去重。

5. 为什么不会漏掉最长序列

因为每条连续序列一定有唯一的起点,也一定会在遍历集合时被访问到。


二十一、整道题的学习路线总结

真正掌握这道题,建议按这个顺序理解:

第一步:先接受排序做法

先通过排序把"数值连续"变成"相邻元素容易判断",建立题感。

第二步:意识到排序不满足 O(n)

这一步很关键,说明开始从"能做对"走向"满足题意最优"。

第三步:想到用哈希做存在性判断

因为连续性本质上就是不断查询某个数在不在。

第四步:理解"只从起点开始扩展"

这是整个 O(n) 解法的灵魂。


二十二、结语

LeetCode 128. 最长连续序列 是一道非常经典的题目,它真正锻炼的不是某个固定模板,而是一种更重要的算法思维:

当排序能让问题变简单时,先用排序建立题感;

当题目进一步要求更优复杂度时,再思考如何用哈希替代排序中的"有序性作用"。

在这道题中:

  • 排序提供了最自然的入门解法
  • 哈希则提供了满足题意的最优解法
  • "只从起点出发"是把复杂度压到 O(n) 的关键技巧

真正理解这三层递进关系,这道题就不再只是背答案,而会成为一道非常有代表性的"从朴素解走向最优解"的经典案例。


相关推荐
Z9fish2 小时前
C语言算法专题总结(一)排序
c语言·算法·排序算法
美式请加冰2 小时前
模拟的介绍和使用
java·开发语言·算法
云泽8082 小时前
蓝桥杯算法精讲:贪心算法之区间问题深度剖析
算法·贪心算法·蓝桥杯
tankeven2 小时前
HJ129 小红的双生数
c++·算法
万能的小裴同学2 小时前
C++ 简易的FBX查看工具
开发语言·c++·算法
Boop_wu2 小时前
[Java 算法] 前缀和(2)
算法·哈希算法·散列表
nqqcat~2 小时前
hlist哈希链表学习笔记
学习·链表·哈希算法
Hello.Reader2 小时前
深入浅出 Adam 优化算法从直觉到公式
深度学习·算法
识君啊2 小时前
拆分与合并的艺术·分治思想:Java归并排序深度解析
java·数据结构·算法·排序算法·归并排序·分治