1. 问题背景与核心挑战
这道力扣hot100中的"最长连续序列"问题(LeetCode 128)是面试中的高频考题,也是检验候选人数据结构基本功的试金石。题目要求在一个未排序的整数数组中找到数字连续的最长序列的长度,且算法时间复杂度必须为O(n)。例如输入[100,4,200,1,3,2],最长连续序列是[1,2,3,4],返回长度4。
这个问题的难点在于:
- 常规排序解法时间复杂度为O(nlogn),不满足要求
- 暴力查找每个数字的连续序列会导致O(n²)复杂度
- 需要设计O(n)的算法,这就必须依赖哈希表这种O(1)查询的数据结构
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 哈希表的选择与实现策略
2.1 为什么选择哈希表?
哈希表(Hash Table)通过键值对存储数据,提供平均O(1)时间复杂度的查找性能。对于这个问题,我们需要频繁查询"某个数字是否存在"以及"它的相邻数字是否存在",这正是哈希表的强项。
Java中有三种主要实现方式:
- HashSet:最简洁的实现,只存储键不存储值
- HashMap:可以存储键值对,本题中不需要
- LinkedHashSet:保持插入顺序,本题不需要
选择HashSet是最合适的,因为:
- 不需要存储额外信息
- 空间复杂度最优
- API简单直接(add/contains)
2.2 算法核心思路
最优解法的步骤如下:
- 将所有数字存入HashSet(去重且O(1)查询)
- 遍历数组,对每个数字检查它是否是序列的起点
- 如何判断起点?当前数字-1不存在于集合中
- 如果是起点,则向后查找连续的数字,统计长度
- 维护一个全局最大值
这个思路的精妙之处在于:
- 通过"前驱不存在"判断起点,避免重复计算
- 每个数字最多被访问两次(一次在遍历,一次在扩展序列)
- 总体时间复杂度严格O(n)
3. Java实现与逐行解析
java复制class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> numSet = new HashSet<>();
for (int num : nums) {
numSet.add(num);
}
int longestStreak = 0;
for (int num : numSet) {
// 只有当num-1不存在时,才作为序列起点
if (!numSet.contains(num - 1)) {
int currentNum = num;
int currentStreak = 1;
// 向后查找连续序列
while (numSet.contains(currentNum + 1)) {
currentNum += 1;
currentStreak += 1;
}
longestStreak = Math.max(longestStreak, currentStreak);
}
}
return longestStreak;
}
}
关键点解析:
- 使用Set而不是直接遍历数组,避免重复元素影响
- 内层while循环只有在找到起点时才会执行
- currentNum和currentStreak的维护方式保证了正确计数
- Math.max的调用位置确保了全局最大值更新
4. 复杂度分析与边界情况
4.1 时间复杂度证明
看似双层循环,但实际时间复杂度确实是O(n):
- 外层循环遍历所有n个数字
- 内层while循环只有在找到序列起点时才会执行
- 每个数字最多被内层循环访问一次(当它属于某个连续序列时)
- 因此总体是O(2n) = O(n)
4.2 空间复杂度
需要额外的HashSet存储所有数字:
- 最坏情况下存储所有n个数字
- 空间复杂度O(n)
4.3 边界情况处理
实际编码中需要注意:
- 空数组输入:应返回0
- 所有数字相同:如[1,1,1],应返回1
- 大数测试用例:注意整型溢出(但本题条件限制数字在int范围内)
- 超大数组:确保不会栈溢出(本题是迭代实现,无此问题)
5. 算法优化与变种思考
5.1 预处理优化
可以在一开始就排除明显不会成为最长序列的数字:
java复制// 添加在第一个for循环之后
if (numSet.size() < 2) {
return numSet.size();
}
5.2 并行处理思路
理论上可以将数字分组后并行处理,但:
- 需要解决结果合并问题
- 对于小数据集反而增加开销
- 面试中不建议提及,除非特别要求
5.3 相似问题拓展
掌握这个解法后,可以解决一系列变种问题:
- 最长连续递增序列(考虑顺序)
- 二维矩阵中的最长连续路径
- 带有权值的最长连续序列
6. 面试实战技巧
6.1 白板编码要点
在白板或共享编辑器上写代码时:
- 先明确说出使用HashSet的意图
- 重点解释为什么检查num-1可以确定起点
- 强调时间复杂度的计算逻辑
- 主动提出边界case并讨论
6.2 常见错误警示
根据面试反馈,候选人常犯的错误:
- 忘记处理空输入
- 使用ArrayList等非O(1)查询的结构
- 错误计算时间复杂度
- 没有去重导致结果错误
6.3 问题延伸讨论
优秀候选人可以进一步讨论:
- 如果数字范围极大但稀疏如何处理
- 分布式环境下如何解决
- 流式数据下的近似算法
7. 实际工程应用场景
虽然这个问题看起来是纯算法题,但其核心思想在实际工程中有广泛应用:
- 用户行为分析:识别连续登录天数
- 运维监控:检测连续失败请求
- 金融风控:发现异常连续交易
- 游戏开发:计算连续成就达成
理解这个算法有助于设计高效的序列检测系统,特别是在实时数据处理场景下,O(n)的时间复杂度至关重要。
8. Java实现中的性能细节
8.1 HashSet初始化优化
默认构造函数创建的HashSet初始容量较小,对于大数组会导致多次扩容:
java复制// 最佳实践:根据数据量预设容量
Set<Integer> numSet = new HashSet<>(nums.length);
8.2 遍历方式选择
使用增强for循环遍历Set比用Iterator略快:
java复制// 比用iterator略高效
for (int num : numSet) {
// ...
}
8.3 自动装箱问题
注意int到Integer的自动装箱开销,对于极端性能场景:
java复制// 如果非常关注性能,可以考虑使用原生int集合库
// 如Eclipse Collections的IntHashSet
9. 单元测试建议
完整的测试用例应该包含:
java复制@Test
public void testLongestConsecutive() {
Solution solution = new Solution();
assertEquals(0, solution.longestConsecutive(new int[]{}));
assertEquals(1, solution.longestConsecutive(new int[]{1}));
assertEquals(1, solution.longestConsecutive(new int[]{1,3,5}));
assertEquals(4, solution.longestConsecutive(new int[]{100,4,200,1,3,2}));
assertEquals(5, solution.longestConsecutive(new int[]{1,2,3,4,5,10,11,12}));
assertEquals(3, solution.longestConsecutive(new int[]{1,2,0,1}));
}
10. 算法可视化理解
为了更直观理解算法,可以想象:
初始数组:[100,4,200,1,3,2]
HashSet存储后:
算法执行过程:
- 遇到1:发现0不存在 → 开始查找1,2,3,4 → 长度4
- 遇到2:发现1存在 → 跳过
- 遇到3:发现2存在 → 跳过
- 遇到4:发现3存在 → 跳过
- 遇到100:发现99不存在 → 查找100 → 长度1
- 遇到200:发现199不存在 → 查找200 → 长度1
最终最大长度为4
