5.7 KiB
5.7 KiB
tags, create time
| tags | create time | |||
|---|---|---|---|---|
|
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["遍历每个数 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)
- 外层遍历: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 nums:
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 nums {
// 只有 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),两者都利用了哈希集合适配性不同的角度来解决。