128. 最长连续序列#
生活中的算法#
你小时候一定玩过数字积木吧?一堆写着不同数字的积木块散落在地上,你会不自觉地想把连续的数字找出来,按顺序排成一行。比如看到3、4、5,就会把它们放在一起,因为它们是连续的。
这就是我们今天要讲的”最长连续序列”问题的生活原型。不过,这道题更有趣的地方在于:它要求我们在不对数组排序的情况下,找出这样的连续序列。就像是在不整理积木的情况下,用眼睛找出最长的连续数字链。
问题描述#
题目目标#
给定一个未排序的整数数组 nums,请找出数字连续的最长序列长度。这里的“连续”只要求数值连续,不要求这些元素在原数组中的位置连续。
示例 1#
输入: nums = [100,4,200,1,3,2]
输出: 4
序列示意:
原数组: [100, 4, 200, 1, 3, 2]
连续序列: [1, 2, 3, 4]
长度: 4text说明: 虽然数组本身是无序的,但数字 1,2,3,4 可以组成最长连续序列,所以答案是 4。
补充说明#
- 题目强调时间复杂度要达到
O(n)。 - 重点不是排序,而是快速判断某个数是否是连续序列的起点。
最直观的解法:排序法#
最容易想到的方法就是:先把数组排序,然后找最长的连续序列。就像我们整理积木时,先把所有积木按数字大小排好,然后就能轻松找到最长的连续数字链。
具体步骤是这样的:
- 将数组排序
- 遍历排序后的数组,记录当前连续序列的长度
- 如果下一个数不连续,就重新开始计数
- 记录所有连续序列中的最大长度
让我们用一个例子来模拟这个过程:
原数组:[100, 4, 200, 1, 3, 2]
排序后:[1, 2, 3, 4, 100, 200]
遍历过程:
1 -> 当前长度=1
2 -> 连续,长度=2
3 -> 连续,长度=3
4 -> 连续,长度=4
100 -> 不连续,重新开始,长度=1
200 -> 不连续,重新开始,长度=1
最长连续序列的长度 = 4plaintext这种思路可以用Java代码这样实现:
public int longestConsecutive(int[] nums) {
// 边界条件:空数组没有任何连续序列,直接返回 0
if (nums.length == 0) return 0;
// 先将数组升序排序,便于线性扫描连续区间
Arrays.sort(nums);
// maxLength:记录历史上遇到的最长连续序列长度
int maxLength = 1;
// currentLength:记录当前连续序列的长度(以 nums[i] 结尾)
int currentLength = 1;
// 从第二个元素开始,与前一个元素比较判断是否连续
for (int i = 1; i < nums.length; i++) {
// 若当前值与前一个值相同,属于重复元素,跳过避免干扰长度计算
if (nums[i] == nums[i-1]) continue;
// 若当前值恰好等于前一个值 + 1,说明连续序列可以延长
if (nums[i] == nums[i-1] + 1) {
// 当前连续长度加 1
currentLength++;
// 同步更新全局最大长度
maxLength = Math.max(maxLength, currentLength);
} else {
// 不连续:以当前元素作为新序列起点,长度重置为 1
currentLength = 1;
}
}
// 返回扫描结束后得到的最长连续序列长度
return maxLength;
}java但这个解法的时间复杂度是O(nlogn),不满足题目要求的O(n)。我们需要想出更巧妙的方法。
优化解法:哈希集合法#
仔细思考一下,每个连续序列都有一个起点(序列中的最小数字)。如果我们能快速判断一个数字是否是某个连续序列的起点,问题就简化了。
哈希集合法的原理#
- 用哈希集合存储所有数字,实现O(1)时间的查找
- 对于每个数字x,如果x-1不存在,说明x可能是一个连续序列的起点
- 从这个起点开始,不断查找下一个数字,直到连续性破坏
- 记录过程中找到的最长序列
算法步骤(伪代码)#
- 创建哈希集合,存入所有数字
- 遍历数组中的每个数字x:
- 如果x-1不在集合中(说明是起点)
- 从x开始,尝试找x+1, x+2, …直到找不到为止
- 更新最长序列的长度
- 返回最长长度
示例运行#
让我们用示例数组[100, 4, 200, 1, 3, 2]模拟运行过程:
首先创建哈希集合:{1, 2, 3, 4, 100, 200}
检查100:
- 99不在集合中,100是起点
- 找101,不存在
- 长度为1
检查4:
- 3在集合中,跳过
检查200:
- 199不在集合中,200是起点
- 找201,不存在
- 长度为1
检查1:
- 0不在集合中,1是起点
- 找2,存在
- 找3,存在
- 找4,存在
- 找5,不存在
- 长度为4
...剩余数字都不是起点,跳过plaintextJava代码实现#
public int longestConsecutive(int[] nums) {
// set:哈希集合,用于 O(1) 平均时间判断某个数字是否存在
Set<Integer> set = new HashSet<>();
// 预处理:把数组中的所有数字加入集合(重复值会被自动去重)
for (int num : nums) {
set.add(num);
}
// maxLength:记录遍历过程中发现的最长连续序列长度
int maxLength = 0;
// 遍历原数组中的每个数字,尝试把它当作某个连续序列的起点
for (int num : nums) {
// 关键剪枝:只有当 num-1 不存在时,num 才可能是一个序列起点
// 如果 num-1 存在,则 num 一定在某个序列中间,跳过可避免重复计数
if (!set.contains(num - 1)) {
// currentNum:从当前起点开始向后探测的数字
int currentNum = num;
// currentLength:以 num 为起点的连续序列当前长度,起步为 1(仅包含 num)
int currentLength = 1;
// 只要 currentNum+1 还在集合中,说明连续序列还可以继续延长
while (set.contains(currentNum + 1)) {
// 向后推进到下一个连续数字
currentNum++;
// 序列长度同步加 1
currentLength++;
}
// 一段连续序列统计完成后,更新全局最大值
maxLength = Math.max(maxLength, currentLength);
}
}
// 返回最终的最长连续序列长度
return maxLength;
}java排序法vs哈希集合法#
让我们比较这两种解法:
排序法的时间复杂度是O(nlogn),空间复杂度是O(1)(如果不考虑排序的空间开销)。它的优点是直观、易于理解,适合处理有序或接近有序的数据。
哈希集合法的时间复杂度是O(n),空间复杂度是O(n)。虽然需要额外的空间来存储哈希集合,但它能满足题目的时间复杂度要求。关键是它通过空间换时间的方式,避免了排序的开销。
题目模式总结#
这道题体现了一个重要的算法思想:通过预处理数据结构来优化查找。
这种思想在很多场景中都很有用,比如:
- 查找数组中是否存在某个元素的补数
- 判断数组中是否存在重复元素
- 检查字符串中的字符是否出现过
解决这类问题的通用思路是:
- 观察问题中的查找模式
- 选择合适的数据结构来优化查找操作
- 在某些情况下,可以用空间换时间来提高效率
小结#
通过这道题,我们不仅学会了如何找最长连续序列,更重要的是理解了”用合适的数据结构优化查找”这一重要思想。有时候,看似复杂的问题,只要换个角度思考,选择合适的数据结构,就能得到优雅的解法。
记住,算法设计不仅是about解决问题,更是about如何优雅高效地解决问题!