Files
leetcode-go/哈希/03-最长连续序列.md

5.8 KiB
Raw Permalink Blame History

tags, create time
tags create time
LeetCode
哈希表
中等
2026-05-13 14:30

03-最长连续序列

题面

给定一个未排序的整数数组 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
解释:最长数字连续序列是 [0, 1, 2, 3, 4, 5, 6, 7, 8]。它的长度为 9。

示例 3:

输入:nums = [1,0,1,2]
输出:3
解释:最长数字连续序列是 [0, 1, 2]。它的长度为 3。

约束:

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

思路

[!question] 💡 思考 排序可以做到 O(n) 扫描找答案,但排序本身是 O(n log n),不符合要求。如何在无序数据中"瞬间判断"某个数是否存在?

方法一:排序后线性扫描 ⚠️(O(n log n),不可取)

先排序再扫描的思路虽然直观,但违背了 O(n) 的限制条件,仅作为理解本题的阶梯。

方法二:哈希集合 + 只从起点展开 ⭐(O(n))

核心洞察:每个连续序列有且只有一个起点——即 x-1 不在数组中的那个 x。

利用这个性质,我们可以只做一件事:当发现某个数是序列起点时,顺着往后数能走多远。非起点的数直接跳过,不会被重复计数。

算法流程

flowchart TD
    A["将 nums 全部放入哈希集合 s"] --> B["遍历集合 s 中的每个数 num"]
    B --> C{"num - 1 在 s 中?"}
    C -->|"是"| D["不是起点, 跳过"]
    C -->|"否"| E["从 num+1 开始, 不断 +1 检查是否在 s 中"]
    E --> F["记当前序列长度 len"]
    F --> G["ans = max(ans, len)"]
    D --> B
    G --> B
    B --> H["遍历结束"]
    H --> I["返回 ans"]

为什么仍是 O(n)?

  • 建集合:O(n)
  • 外层遍历:遍历 set 的 key,最多 n 次迭代(重复元素已被去重,不会产生多余循环)
  • 内层 while 展开:每个元素最多被内层访问一次——因为只有它是起点时才会进入 while,而一旦某个数被 while 访问过,后续任何枚举到它时都必然有 num-1 也在集合中,不会再次展开。

两个循环加起来,每个元素的总访问次数是常数次,因此整体 O(n)。

以 nums = [100, 4, 200, 1, 3, 2] 为例:

枚举值 num-1 存在? 动作 连续长度
100 101 ❌ → 实际查 99 ❌ 不是起点,跳过 —
4 3 ✅ 不是起点,跳过 —
200 199 ❌ 是起点! 向后展开:200→201(不存在) 1
1 0 ❌ 是起点! 向后展开:1→2→3→4(5不存在) 4
3 2 ✅ 不是起点,跳过 —
2 1 ✅ 不是起点,跳过 —

最终答案:4

关键细节:

  • Go 中用 map[int]struct{} 作为 set,struct{}{} 零内存开销
  • 需要先处理空数组边界情况:len(nums) == 0 时返回 0
  • 重复元素不影响正确性:集合天然去重,每个数值只保留一份

代码提示

// 伪代码模板
set = 将 nums 所有元素放入哈希集合
ans = 0

for each num in set:
    if (num - 1) NOT in set:
        // num 是某个连续序列的起点
        current = num + 1
        len = 1
        while current in set:
            len++
            current++
        ans = max(ans, len)

return ans

Go 语言中使用 struct{} 构造哈希集合:

s := make(map[int]struct{})
for _, v := range nums {
    s[v] = struct{}{}
}

// 判断 key 是否存在
if _, ok := s[key]; !ok {
    // key 不存在
}

技巧

[!tip] 🔑 模式:只从起点展开 这是「集合去重 + 单侧展开」的经典手法。关键点在于理解每个连续段有唯一的左端点,这样能保证每条边(相邻整数对)只在一次展开中被遍历,从而证明总时间复杂度为 O(n)。

[!note] 🐹 Go Set 的最佳实践

  • map[T]struct{} 比 map[T]bool 更省内存(struct{}{} 大小为 0)
  • 查找语法:_, ok := m[k];删除语法:delete(m, k)
  • 不需要初始化 capacity,但如果已知去重后数量,可以 make(map[int]struct{}, n) 减少扩容

[!info] 📊 复杂度分析

  • 时间:O(n),建集合 O(n) + 外层遍历 O(n) + 内层展开均摊 O(1)(每个元素只被 while 访问一次)
  • 空间:O(n),哈希集合存储去重后的所有元素

[!danger] ⚠️ 常见陷阱

  • 漏掉空数组:nums = [] 应返回 0,而非 panic
  • 没有从起点展开:如果不对 num-1 做判断,最坏情况下内层 while 会对每个元素都执行,退化为 O(n²)
  • 没去重:如果直接用切片存而不先去重,重复元素会导致长度计算错误(如 [1,1,2,2,3] 答案应为 3 而非 5)

代码

func longestConsecutive(nums []int) int {
	if len(nums) == 0 {
		return 0
	}

	// 构建哈希集合(自动去重)
	set := make(map[int]struct{})
	for _, v := range nums {
		set[v] = struct{}{}
	}

	ans := 0
	for num := range set {
		// 只有 num-1 不在集合中时,num 才是连续序列的起点
		if _, ok := set[num-1]; !ok {
			current := num + 1
			length := 1
			for ; ; length++ {
				if _, ok := set[current]; !ok {
					break
				}
				current++
			}
			if length > ans {
				ans = length
			}
		}
	}

	return ans
}

[!success] ✅ 运行验证 这是 LeetCode 第 128 题,Hard 难度但核心思想简洁。类似变体包括「缺失的第一个正数」(LC 41),两者都利用了哈希集合适配性不同的角度来解决。