题目描述
给定一个未排序的整数数组 nums,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
需要设计并实现时间复杂度为 O(n) 的算法。
最初思路
最开始的整体方向是:
- 用
HashSet对原数组去重。 - 再准备一张
HashMap,遍历集合时检查x-1、x+1是否已经出现。 - 每发现一个相邻数字,就把全局计数器
cnt加一。
去重和「看相邻数字」这个方向是对的。真正要求的是最长那一段的长度,而不是数组里所有相邻关系的总和。
问题出在哪里
第一版用一个贯穿整个循环的 cnt,每次看到相邻关系就 cnt++。以 [1, 2, 10, 11] 为例,两段长度都是 2,但 cnt 会把两段的相邻关系加在一起,得到的不是最长段。
中间又改成「找到起点后把 cnt 重置为 1,再检查一次 x+1」。这会带来两个新问题:
HashSet的遍历顺序不是从小到大。遇到新起点时直接cnt = 1,前面已经数过的更长段会被清掉。例如[1, 2, 6, 7, 8],如果先扫完6,7,8得到 3,再遇到起点1,最终留下的可能是最后扫到的那一段,而不是最长的 3。- 每一轮只检查旁边的
x+1一次。长度大于 2 的段,不能靠「当前这个x再往后看一格」数完;而且HashSet无序,也不能指望x=7时再把8补进去。
另外,起点那一轮如果先 cnt = 1 再 cnt++,紧接着又因为 x+1 存在再加一次,[1,2,3,4] 会把起点 1 重复计入。
正确思路
把数字放入 HashSet 去重后,遍历集合中的每个 x:
- 如果
x-1也在集合里,说明x不是这段的起点,直接跳过。 - 否则从
x开始,令y = x + 1,不断向后走,直到y不在集合中。 - 当前段长度是
y - x,用ans取历史最大值。
空数组时集合为空,循环不执行,ans 保持 0,不必单独特判。
核心洞察
一段连续序列只有一个真正的起点:集合里存在 x,但不存在 x-1。
只从这样的 x 向后扫描,每段只会被数一次。如果对每个数字都向后 while,像 [1, 2, ..., n] 这种输入里,1 会扫 n 步,2 会扫 n-1 步,总时间会退化成 O(n²)。
HashSet 遍历无序,所以不能靠「当前段最小值大于上一段最大值」来分段,也不能用一个全局 cnt 一路往上加。每段的长度要在这一段扫完时当场算出来,再和全局答案取最大。
手推过程
以 nums = [1, 2, 6, 7, 8] 为例。集合为 {1, 2, 6, 7, 8},ans 初始为 0。
x = 1:1-1 = 0 不在集合中,是起点
y 从 2 走到 3,3 不在集合中
段长 = 3 - 1 = 2
ans = max(0, 2) = 2
x = 2:2-1 = 1 在集合中,跳过
x = 6:6-1 = 5 不在集合中,是起点
y 从 7 走到 9,9 不在集合中
段长 = 9 - 6 = 3
ans = max(2, 3) = 3
x = 7:7-1 = 6 在集合中,跳过
x = 8:8-1 = 7 在集合中,跳过
无论集合按什么顺序遍历,1 和 6 各自作为起点只会被完整扫描一次,最终 ans 仍是 3。
再看 [1, 2, 10, 11]:两段都是 2,ans 取最大值后仍为 2,不会把两段加在一起。
伪代码
下面的伪代码和 Java 实现一一对应:
函数 longestConsecutive(nums):
创建集合 m
将 nums 中的每个数加入 m
ans <- 0
对于 m 中的每个 x:
如果 m 中包含 x - 1:
跳过
y <- x + 1
当 m 中包含 y:
y <- y + 1
ans <- max(ans, y - x)
返回 ans
Java 代码
class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> m = new HashSet<>();
for (int x : nums) {
m.add(x);
}
int ans = 0;
for (int x : m) {
if (m.contains(x - 1))
continue;
int y = x + 1;
while (m.contains(y)) {
y++;
}
ans = Math.max(ans, y - x);
}
return ans;
}
}
易错点
- 要统计的是最长那一段,不是所有相邻关系的总和。全局
cnt一路累加会把多段拼在一起。 - 遇到新起点时不能只重置当前计数,必须用
Math.max保留已经出现过的更长段。 HashSet遍历无序,不能依赖「上一段」或「当前段最小值大于上一段最大值」来切分。- 只检查一次
x+1数不完长度大于 2 的段;要从起点用while一直走到断掉。 - 起点那一轮不要把
x加两次。用y - x计算段长,可以避开手动cnt++的重复计数。 - 必须先判断
x-1不存在再向后扫描,否则复杂度会退化。
建议测试用例
nums = []
期望:0
nums = [1]
期望:1
nums = [1, 2, 10, 11]
期望:2
说明:两段互不相邻,答案是最长段,不是两段之和
nums = [1, 2, 6, 7, 8]
期望:3
说明:无论先扫到哪一段,最终都要留下更长的那一段
nums = [100, 4, 200, 1, 3, 2]
期望:4
nums = [1, 2, 0, 1]
期望:3
说明:HashSet 去重后是 0,1,2
复杂度分析
设去重后的数字个数为 n。
每个数字进入 HashSet 一次。外层遍历每个数字时,若它不是起点则直接跳过;while 只从起点往后走,每个数字最多作为 y 被检查一次。因此:
- 时间复杂度:
O(n)。 - 空间复杂度:
O(n),用于保存HashSet。
如果去掉「x-1 不存在才扫描」这个判断,对 [1, 2, ..., n] 会变成 O(n²)。
总结
这道题的关键不是把相邻数字都数一遍,而是先认出每一段的起点。x-1 不在集合中时,从 x 向后走到断掉,用 y - x 得到段长,再和全局答案取最大。
本次主要问题有两层:一开始把多段长度加在了一起;后来虽然找到了起点,但既没有保留历史最长,也没有从起点把整段扫完。遇到类似「无序集合里找连续段」的问题时,可以先问自己:当前这个数是不是这段的起点?