【LeetCode Hot 100】#128 最长连续序列:HashSet 的一题双解与 Fail-Fast 避坑

作者:war 发布时间: 2026-09-23 阅读量:5 评论数:0

题目描述

给定一个未排序的整数数组 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

示例 3:

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

提示:

  • 0 <= nums.length <= 10^5
  • -10^9 <= nums[i] <= 10^9

分析1

我们可以将这里的的数字分类,分为三类:

  • 头:该数字没有前置数字
  • 中:该数字存在前置和后续数字
  • 尾:该数字没有后续数字

我们这的任务就是将所有的头节点找到(假设为 x ),然后从头节点找到下一个 x+1 、x+2 ... x+n ,直到找到尾节点(没有 x+m 的数字了),这就成为了一个完整的连续序列了。这样就可以与之前找的序列的长度进行比较,找到最长的那个就是我们需要的最后答案。

解答1

class Solution {
    public int longestConsecutive(int[] nums) {
        Set<Integer> set = new HashSet<>();
        int res = 0;

        // 将数据添加到set里进行去重
        for(int n:nums)
            set.add(n);

        // 遍历整个set
        for(int num:set){
            // 检测当前数字是不是最前面的数字
            if(!set.contains(num-1)){
                int currentNum = num;
                int currentStreak = 1;  // 记录当前长度

                // 一直判断是否存在下一个连续的数字
                while(set.contains(currentNum+1)){
                    currentNum = currentNum + 1;
                    currentStreak += 1;
                }

                // 判断完成,得到当前最长
                res = res > currentStreak ? res : currentStreak;

            }
        }
        return res;
    }
}

复杂度分析

  • 时间复杂度:将数据添加到set里,复杂度为 O(n) ,遍历整个set复杂度为 O(m),其中 m 为不同数字的个数 m <= n,只有连续序列的“起点”才会进入 while 循环,故总体时间复杂度为 O(n)。
  • 空间复杂度:HashSet 最多存储 n 个不同数字,因此需要 O(n) 的额外空间。

为什么要用 HashSet ?

  1. HashSet 可以去重,保证每个数字唯一。
  2. 底层是哈希表,contains、add、remove 的平均时间复杂度都是 O(1)。这样才能保证不管序列有多长,我们“找下一个数”和“判断前一个数”的操作都是瞬间完成的。
  3. 如果用数组会发生什么?数组的 contains 操作的时间复杂度是 O(n) ,这样就会导致整个总时间复杂度变成的 O(n^2) 。

为什么时间复杂度是 O(N)?

这里的可以看到 for 循环里还有一个 while 循环,可能会想这个是不是 O(n^2) 的时间复杂度。但实际上只有外层 for 循环遍历的是去重后的 set,执行了 m 次(m <= n),而 while 循环只会对每个连续序列的“起点”执行,而且每个数字在 while 里最多被访问一次。

下面举个例子来说明为什么每个数字在 while 里最多被访问一次:

假设数组是 nums = [1, 2, 3, 4, 5]。我们假设 set 的遍历顺序恰好是 1, 2, 3, 4, 5。实际上 HashSet 的遍历顺序不确定,但无论顺序如何,每个数字最多被 while 访问一次,结论不变。

执行过程拆解:

第一步:外层 for 循环遍历到 1

  • 判断:num - 1 也就是 0 在集合里吗?不在。

  • 所以 1 是起点,进入 while 循环。

  • while 循环内部:

    • 检查 2 在不在?在。currentNum 变成 2。
    • 检查 3 在不在?在。currentNum 变成 3。
    • 检查 4 在不在?在。currentNum 变成 4。
    • 检查 5 在不在?在。currentNum 变成 5。
    • 检查 6 在不在?不在。退出 while。
  • 这一次 while 循环,检查了 2, 3, 4, 5。总共执行了 4 次。

第二步:外层 for 循环遍历到 2

  • 判断:num - 1 也就是 1 在集合里吗?在!
  • 所以 2 不是起点。直接跳过,根本不进入 while 循环。

第三步:外层 for 循环遍历到 3

  • 判断:num - 1 也就是 2 在集合里吗?在!
  • 直接跳过。

第四步、第五步:遍历到 4 和 5

  • 同理,前面的数字都在,全部直接跳过。

2, 3, 4, 5 这四个数字,只在 1 触发的那个 while 循环中被访问了一次。 当外层 for 循环后来遍历到它们时,因为前面有数字,它们被 if (!set.contains(num - 1)) 这个条件直接拦截了,根本没有机会触发自己的 while 循环。

所以最后计算时间复杂度时,就是 O(n) 。

分析2

这是在该题的leetcode的评论区看到的,解法思路从 #200 岛屿数量 而来。原题主要考的是BFS/DFS,一个二维的地图,借助其思路,可以这样进行假设:将我们要求的数假设为一维的地图,我们先登上其中一块岛屿(选一个数),然后观察左右有没有相邻的岛屿(连续的数字),分别向两边走,然后记录走的总长度,走过的就“淹没”(从集合中删除),下次不再走。

解答2

class Solution {
    public int longestConsecutive(int[] nums) {
        Set<Integer> set = new HashSet<>();
        int res = 0;

        // 将数据添加到set里进行去重
        for(int n:nums)
            set.add(n);

        // 遍历整个数组(扫描整片海域)
        for(int num:nums){
            int currentStreak = 0;

            // 如果这个数字还在集合里(说明它还没被探索过,是一块新岛屿的入口)
            if(set.contains(num)){
                set.remove(num);   // 访问过后就“淹没”
                currentStreak = 1;

                // 向左探索
                int temp = num - 1;
                while(set.contains(temp)){
                    set.remove(temp);
                    currentStreak++;
                    temp--; // 继续左走
                }

                // 向右探索
                int temp2 = num + 1;
                while(set.contains(temp2)){
                    set.remove(temp2);
                    currentStreak++;
                    temp2++; // 继续右走
                }

                // 更新最长岛屿长度
                res = res > currentStreak ? res : currentStreak;
            }
        }
        return res;
    }
}

复杂度分析:

  • 时间复杂度:将数据添加到set里,复杂度为 O(n) ,遍历整个数组复杂度为 O(n) ,每个数字一旦被 set.remove(num) 移除,之后就不会再进入扩展分支。while 向左、向右扩展时,也会把访问过的数字从集合中移除。因此每个数字最多被加入一次、被移除一次、在扩展中被访问一次。所有 while 循环的总执行次数不超过 n,所以整体仍然是 O(n) 。
  • 空间复杂度:HashSet 最多存储 n 个不同数字,因此需要 O(n) 的额外空间。

为什么遍历 nums 而不是遍历 set?

为什么我们要在

// 遍历整个数组(扫描整片海域)
        for(int num:nums){

遍历数组而不是直接遍历 set 呢?如果遍历了 set 那不就不用下面再判断是否存在于 set 了吗?

因为在 Java 中,如果你使用增强 for 循环(for (int num : set)),编译器会把它转换成使用 Iterator(迭代器) 来遍历。

Java 的集合类(如 HashSet、ArrayList)都实现了 Fail-Fast 机制。这意味着:

迭代器在遍历时,内部会维护一个版本号(modCount,修改次数)。

每次调用 iterator.next() 时,都会检查当前的 modCount 是否等于迭代器刚创建时的 expectedModCount。

如果你在循环体内直接调用了 set.remove(),集合的 modCount 就会加 1。

下一次循环迭代器调用 next() 时,发现 modCount != expectedModCount,就会立刻抛出 ConcurrentModificationException,程序直接崩溃。

所以千万不能像下面这样写

for (int num : set) { // 底层使用 Iterator
    if (set.contains(num)) {
        set.remove(num); // 抛出 ConcurrentModificationException!
    }
}

image-vClW.webp

为什么遍历 nums 就是安全的?
nums 是一个普通的数组(int[])。
当使用 for (int num : nums) 遍历数组时,底层并不是迭代器,而是普通的 for (int i = 0; i < nums.length; i++) 索引遍历。数组没有 modCount 检查机制。

评论