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

200 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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
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{}` 构造哈希集合:
```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
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),两者都利用了哈希集合适配性不同的角度来解决。