1. 问题背景与核心挑战
第一次在力扣上刷到"最长连续序列"这道题时,我盯着示例输入[100,4,200,1,3,2]看了半天——看似简单的数字排列,要找出最长的连续数字序列长度(这个例子中是4,对应序列[1,2,3,4])。作为hot100中的经典题目,它考察的核心是对哈希集合的巧妙运用,以及如何用O(n)时间复杂度解决问题。
大多数人在面试中遇到这道题,第一反应可能是先排序再遍历。但排序需要O(nlogn)时间,而题目明确要求设计O(n)算法。这时候哈希表(在Java中通常是HashSet)就派上用场了。不过哈希解法有几个关键细节需要注意:如何避免重复计算?怎样确定序列的起点?这些都是决定算法效率的关键。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 哈希解法核心思路拆解
2.1 为什么选择哈希集合
使用HashSet存储所有数字后,我们可以在O(1)时间内查询任意数字是否存在。对于序列[1,2,3,4],如果我们能快速判断2、3、4是否存在,就能避免排序带来的时间开销。这就是空间换时间的典型应用——用O(n)额外空间换取O(n)时间复杂度。
2.2 序列起点判定技巧
关键观察:一个数字x如果是某个连续序列的起点,那么x-1一定不存在于数组中。例如在[100,4,200,1,3,2]中:
- 1是起点(0不存在)
- 100是起点(99不存在)
- 200是起点(199不存在)
基于这个特性,我们可以先遍历数组将所有数字存入HashSet,然后再次遍历,只有当遇到潜在起点时才向后探索序列长度。
3. Java实现与优化细节
3.1 基础实现代码
java复制public int longestConsecutive(int[] nums) {
Set<Integer> numSet = new HashSet<>();
for (int num : nums) {
numSet.add(num);
}
int longestStreak = 0;
for (int num : numSet) {
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;
}
3.2 时间复杂度分析
虽然代码中有嵌套循环,但每个数字最多被访问两次(一次在外部循环,一次在内部while循环),所以整体时间复杂度是O(2n)即O(n)。空间复杂度是O(n)用于存储哈希集合。
3.3 边界条件处理
实际编码时需要特别注意:
- 空数组输入应返回0
- 所有数字相同的情况(如[0,0]应返回1)
- 大数测试用例(考虑整数溢出问题)
4. 算法优化与变种
4.1 只遍历一次的优化
其实可以只遍历一次数组,在添加元素到HashSet的同时维护序列长度。这种写法虽然时间复杂度相同,但在某些情况下可以减少实际运行时间:
java复制public int longestConsecutive(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
int max = 0;
for (int num : nums) {
if (!map.containsKey(num)) {
int left = map.getOrDefault(num - 1, 0);
int right = map.getOrDefault(num + 1, 0);
int sum = left + right + 1;
map.put(num, sum);
max = Math.max(max, sum);
// 更新边界值
map.put(num - left, sum);
map.put(num + right, sum);
}
}
return max;
}
4.2 并查集解法
这道题也可以用并查集(Union-Find)来解决,将连续的数字合并到同一个集合中。虽然时间复杂度理论上是O(nα(n)),但实际表现也很优秀:
java复制class UnionFind {
private Map<Integer, Integer> parent;
private Map<Integer, Integer> size;
public UnionFind(int[] nums) {
parent = new HashMap<>();
size = new HashMap<>();
for (int num : nums) {
parent.put(num, num);
size.put(num, 1);
}
}
// 省略find和union方法实现
}
// 在主函数中使用UnionFind统计最大集合大小
5. 常见错误与调试技巧
5.1 新手容易踩的坑
- 直接排序解法:虽然能通过部分测试用例,但无法满足O(n)要求
- 重复计算:没有判断序列起点导致O(n²)时间复杂度
- 边界处理不当:忽略空数组或全相同数字的情况
- 哈希表使用错误:用HashMap代替HashSet造成空间浪费
5.2 调试建议
当你的代码出现问题时,可以尝试这些测试用例:
- [] → 应返回0
- [0,0] → 应返回1
- [1,2,0,1] → 应返回3
- [9,1,4,7,3,-1,0,5,8,-1,6] → 应返回7
提示:在IDE中调试时,可以在while循环内打印currentNum和currentStreak的值,直观观察序列扩展过程。
6. 实际应用场景延伸
这种寻找连续序列的思路在实际开发中有很多应用场景:
- 用户行为分析:找出连续登录天数
- 库存管理:识别连续缺失的商品编号
- 时间序列处理:检测连续的时间段
- 游戏开发:计算连续达成成就的天数
理解这个算法后,可以很容易地将其适配到这些业务场景中。比如要统计用户连续登录天数,只需要将数字换成时间戳即可。
