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