200 lines
5.7 KiB
Markdown
200 lines
5.7 KiB
Markdown
---
|
||
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),两者都利用了哈希集合适配性不同的角度来解决。
|