给定一个未排序的整数数组,找出最长连续序列的长度。
要求算法的时间复杂度为 O(n)。
示例:
输入: [100, 4, 200, 1, 3, 2]
输出: 4
解释: 最长连续序列是 [1, 2, 3, 4]。它的长度为 4。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-consecutive-sequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
因为一个序列可能在 nums 数组的任意一个数字开始,我们可以枚举每个数字作为序列的第一个数字,搜索所有的可能性。
暴力算法没有任何思路或技巧上的优化,它仅仅将 nums 数组中的每一个数字,作为序列第一个数字,枚举后面的数字,直到有数字在原数组中没出现过。当枚举到数组中没有的数字时(即 currentNum 是一个数组中没出现过的数字),记录下序列的长度,如果比当前最优解大的话就更新。算法一定能找到最优解,因为它枚举了每一种可能性。
class Solution {
private boolean arrayContains(int[] arr, int num) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == num) {
return true;
}
}
return false;
}
public int longestConsecutive(int[] nums) {
int longestStreak = 0;
for (int num : nums) {
int currentNum = num;
int currentStreak = 1;
while (arrayContains(nums, currentNum + 1)) {
currentNum += 1;
currentStreak += 1;
}
longestStreak = Math.max(longestStreak, currentStreak);
}
return longestStreak;
}
}
时间复杂度:O(n^3)
外部循环运行恰好 n 次。同时,因为 currentNum 在 while 循环中每次增加 1 ,所以 while 循环会运行 O(n) 次。在每次 while 循环中,会有一个对数组 O(n)的查找。所以暴力算法是一个嵌套三层 O(n) 的循环,总时间复杂度是 O(n^3)。
空间复杂度:O(1)
暴力算法仅使用了有限的整数,所以它用了常数级别的额外空间。
如果我们可以将数组中的数字升序迭代,找连续数字会变得十分容易。为了将数组变得有序,我们将数组进行排序。
在我们开始算法之前,首先检查输入的数组是否为空数组,如果是,函数直接返回 0 。对于其他情况,我们将 nums 数组排序,并考虑除了第一个数字以外的每个数字与它前一个数字的关系。如果当前数字和前一个数字相等,那么我们当前的序列既不会增长也不会中断,我们只需要继续考虑下一个数字。如果不相等,我们必须要检查当前数字是否能延长答案序列(也就是 nums[i] == nums[i-1] + 1)。如果可以增长,那么我们将当前数字添加到当前序列并继续。否则,当前序列被中断,我们记录当前序列的长度并将序列长度重置为 1 。由于 nums 中的最后一个数字也可能是答案序列的一部分,所以我们将当前序列的长度和记录下来的最长序列的长度的较大值返回。
如上是一个例子。在做线性查找所有连续序列之前,先将数组进行了排序。 标红的是最长连续序列。
class Solution {
public int longestConsecutive(int[] nums) {
if (nums.length == 0) {
return 0;
}
Arrays.sort(nums);
int longestStreak = 1;
int currentStreak = 1;
for (int i = 1; i < nums.length; i++) {
if (nums[i] != nums[i-1]) {
if (nums[i] == nums[i-1]+1) {
currentStreak += 1;
}
else {
longestStreak = Math.max(longestStreak, currentStreak);
currentStreak = 1;
}
}
}
return Math.max(longestStreak, currentStreak);
}
}
时间复杂度:O(nlgn)
算法核心的 for循环恰好运行 nn 次,所以算法的时间复杂度由 sort 函数的调用决定,通常会采用 O(nlgn)O(nlgn) 时间复杂度的算法。
空间复杂度:O(1)(或者 O(n))
以上算法的具体实现中,由于我们将数组就低排序,所以额外的空间复杂度是常数级别的。如果不允许修改输入数组,我们需要额外的线性长度的空间来保存中间结果和排好序的数组。
其实我们一开始的暴力解法的思路是正确的,但是需要进行一些优化,才能达到 O(n)O(n) 的时间复杂度。
这个优化算法与暴力算法仅有两处不同:这些数字用一个 HashSet 保存(或者用 Python 里的 Set),实现 O(1) 时间的查询,同时,我们只对 当前数字 - 1 不在哈希表里的数字,作为连续序列的第一个数字去找对应的最长序列,这是因为其他数字一定已经出现在了某个序列里。
class Solution {
public int longestConsecutive(int[] nums) {
Set num_set = new HashSet();
for (int num : nums) {
num_set.add(num);
}
int longestStreak = 0;
for (int num : num_set) {
if (!num_set.contains(num-1)) {
int currentNum = num;
int currentStreak = 1;
while (num_set.contains(currentNum+1)) {
currentNum += 1;
currentStreak += 1;
}
longestStreak = Math.max(longestStreak, currentStreak);
}
}
return longestStreak;
}
}
时间复杂度:O(n)
尽管在 for 循环中嵌套了一个 while 循环,时间复杂度看起来像是二次方级别的。但其实它是线性的算法。因为只有当 currentNum 遇到了一个序列的开始, while 循环才会被执行(也就是 currentNum-1 不在数组 nums 里), while 循环在整个运行过程中只会被迭代 n 次。这意味着尽管看起来时间复杂度为 O(n⋅n) ,实际这个嵌套循环只会运行 O(n + n) = O(n) 次。所有的计算都是线性时间的,所以总的时间复杂度是 O(n) 的。
空间复杂度:O(n)
为了实现 O(1) 的查询,我们对哈希表分配线性空间,以保存 nums 数组中的 O(n) 个数字。除此以外,所需空间与暴力解法一致。
作者:LeetCode
链接:https://leetcode-cn.com/problems/two-sum/solution/zui-chang-lian-xu-xu-lie-by-leetcode/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
不难,一开始想用HashMap的,后来感觉java没有指针,值不太好更新,所以直接HashSet走两遍算了。结果和官方题解写的差不多。
执行用时 :14 ms, 在所有Java提交中击败了68.32%的用户
内存消耗 :39 MB, 在所有Java提交中击败了52.43%的用户
class Solution {
public int longestConsecutive(int[] nums) {
HashSet<Integer> hs = new HashSet<>();
for(int i:nums){
hs.add(i);
}
int max=0;
int count=0;
for(int i:hs){
if(!hs.contains(i-1)){
int num = i;
count++;
while(hs.contains(num+1)){
count++;
num++;
}
}
if(count>max) max=count;
count=0;
}
return max;
}
}
看了一下最快的答案——3ms——居然是用sort做的。。。又一次证明了理论上的复杂度优势抵不过每一步时间常数项的大小带来的损耗呀~ 建个hash表还是不适合这种数据少的情况。