刷到这道题的时候,我第一反应:这不排个序就完事了?遍历一遍数连续数字的长度,记录最大值,多简单啊。结果定睛一看题目要求时间复杂度 O (n),瞬间人傻了 ------ 排序本身都 O (nlogn) 了,这不是明摆着不让用排序嘛。
今天就聊聊这道经典题,从排序的朴素解法,到真正满足要求的 O (n) 解法,我把我踩过的坑都给你们捋一遍。
先说说题目到底要干啥
一句话说清题意:给你一个未排序的整数数组,找出数字连续的最长序列的长度。序列元素不用在原数组里挨在一起,只要数字本身是连续的就行。比如输入 [100,4,200,1,3,2],最长连续序列是 1、2、3、4,输出 4。
我的第一版:排序大法(能过但不对)
说实话,我第一反应写的就是排序。思路特别直白:先把数组从小到大排好序,然后从头往后遍历。如果当前数比前一个大 1,就说明连续,长度 +1;不然就重置长度,全程记录最大的长度值。
这个写法跑示例全对,提交也能 AC,但严格来说不符合题目 O (n) 的要求。面试的时候你这么写,面试官大概率会追问一句 "有没有时间复杂度更低的写法?"属于 "保底可以,但冲不了高分" 的解法。
正解来了:哈希集合 + 起点判断
想了好一会儿我才反应过来:要 O (1) 判断一个数字存不存在,那肯定用哈希集合啊。但光有集合还不够 ------ 总不能每个数字都往后挨个查有没有连续数吧,那不又退化成 O (n²) 了。
这里最关键的一个思路点,我当初琢磨了半天才想通:我们只从「连续序列的起点」开始计数。
什么叫起点?如果一个数字 num,它的前一个数 num - 1 不在集合里,那它一定是某段连续序列的开头。反过来,如果 num - 1 存在,那 num 肯定是某段序列中间的一个数,不用管它,等遍历到起点的时候自然会数到它。
就拿示例来说:集合里有 1、2、3、4、100、200。1 的前面 0 不存在,所以 1 是起点;2 的前面 1 存在,所以 2 不是起点,直接跳过。
这样一来,每个数字最多只会被访问一次。只有起点会进入内层循环往后数,中间的数都直接跳过,总的操作次数加起来就是 n 次,时间复杂度自然就是 O (n)。
我一开始还纳闷,这不也是嵌套循环吗?怎么就 O (n) 了?后来自己手算了一遍才明白:不是每个元素都会进内层循环,进循环的只有每个序列的第一个数,整体摊下来还是线性的。
代码实现
直接上 JavaScript 版本,亲测能提交通过:
javascript
var longestConsecutive = function(nums) {
// 空数组直接返回0,别问,问就是在这里错过一次
if (nums.length === 0) return 0
// 用Set去重 + 实现O(1)查找
const numSet = new Set(nums)
let maxLen = 0
for (const num of numSet) {
// 只有前一个数不存在时,才是序列的起点
if (!numSet.has(num - 1)) {
let currentNum = num
let currentLen = 1
// 从起点开始,往后数连续的数字
while (numSet.has(currentNum + 1)) {
currentNum++
currentLen++
}
// 更新最大长度
maxLen = Math.max(maxLen, currentLen)
}
}
return maxLen
};
说个小细节:这里遍历的是 Set 而不是原数组。这样天然就避开了重复元素的问题,比如 [1,0,1,2] 这种有重复的情况,不会因为两个 1 重复计算。
复杂度分析
时间复杂度 O (n):每个元素最多被访问一次,整体线性时间。空间复杂度 O (n):需要一个哈希集合存储所有元素。
聊聊我踩过的坑
- 上来就写排序,写完才想起题目要 O (n),白忙活了好一会儿。
- 最开始写哈希版的时候,每个数都进循环往后数,直接超时,完全没达到 O (n) 的效果。
- 忘了处理空数组的边界情况,提交直接报错,社死现场。
最后碎碎念
其实这题本身逻辑不难,难的是跳出 "排序" 的惯性思维。那个 "只从起点开始遍历" 的思路,我觉得是这题最精髓的地方 ------ 一下子就把嵌套循环的时间复杂度拉回了线性。
你第一次做这道题的时候,第一反应是排序吗?或者你有别的更有意思的解法?评论区聊聊,我每条都会看~
觉得有帮助的话,点个赞让更多小伙伴看到呀