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

200 lines
5.8 KiB
Markdown
Raw Normal View History

2026-05-13 11:52:39 +08:00
---
tags: ["LeetCode", "哈希表", "中等"]
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`**。
利用这个性质,我们可以只做一件事:当发现某个数是序列起点时,顺着往后数能走多远。非起点的数直接跳过,不会被重复计数。
#### 算法流程
```mermaid
flowchart TD
2026-05-13 12:05:09 +08:00
A["将 nums 全部放入哈希集合 s"] --> B["遍历集合 s 中的每个数 num"]
2026-05-13 11:52:39 +08:00
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)
2026-05-13 12:05:09 +08:00
- **外层遍历**:遍历 set 的 key,最多 n 次迭代(重复元素已被去重,不会产生多余循环)
2026-05-13 11:52:39 +08:00
- **内层 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
2026-05-13 12:05:09 +08:00
for each num in set:
2026-05-13 11:52:39 +08:00
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{}` 构造哈希集合:
```go
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)
---
## 代码
```go
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
2026-05-13 12:05:09 +08:00
for num := range set {
2026-05-13 11:52:39 +08:00
// 只有 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),两者都利用了哈希集合适配性不同的角度来解决。