128. 最长连续序列
【题目】:
【代码】:要求复杂度O(n),排序肯定不行,想到用并查集。
- 先把数组中每个数放到HashMap中,key为该数,value都为1。
- 遍历数组nums,递归查找数num的前一个数,找到就修改map中的value。例如nums={100,5,200,4,3,6},遍历到5的时候,查找map中是否存在4,存在则继续查找3,3存在则继续查找2,2不存在即返回0,修改key=3的value=1,key=4的value=2,key=5的value=3。继续遍历数组nums,当找到6的时候,继续找6的前一个数5,key=5的value=3>1,停止查找,返回key=6的value=4。
- 遍历map,找到最大的value,返回即为最长连续序列的长度。
class Solution {
public int longestConsecutive(int[] nums) {
if(nums.length<=1){
return nums.length;
}
Map<Integer, Integer> map=new HashMap<>();
for(int num: nums){
map.put(num,1);
}
for(int num: nums){
forward(map, num);
}
return maxCount(map);
}
private int forward(Map<Integer, Integer> map, int num){
if(!map.containsKey(num)){
return 0;
}
int count=map.get(num);
if(count>1){
return count;
}
count=forward(map, num-1)+1;
map.put(num, count);
return count;
}
private int maxCount(Map<Integer, Integer> map){
int max=0;
for(int key: map.keySet()){
max=Math.max(max, map.get(key));
}
return max;
}
}
以下是LeetCode的官方题解:
- 链接:https://leetcode-cn.com/problems/two-sum/solution/zui-chang-lian-xu-xu-lie-by-leetcode/
- 来源:力扣(LeetCode)
方法 1:暴力
想法
因为一个序列可能在 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)
暴力算法仅使用了有限的整数,所以它用了常数级别的额外空间。
方法 2:排序
想法
如果我们可以将数组中的数字升序迭代,找连续数字会变得十分容易。为了将数组变得有序,我们将数组进行排序。
算法
在我们开始算法之前,首先检查输入的数组是否为空数组,如果是,函数直接返回 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循环恰好运行 n 次,所以算法的时间复杂度由 sort 函数的调用决定,通常会采用 O(nlgn)时间复杂度的算法。
- 空间复杂度:O(1)(或者 O(n))
以上算法的具体实现中,由于我们将数组就低排序,所以额外的空间复杂度是常数级别的。如果不允许修改输入数组,我们需要额外的线性长度的空间来保存中间结果和排好序的数组 。
方法 3:哈希表和线性空间的构造
想法
其实我们一开始的暴力解法的思路是正确的,但是需要进行一些优化,才能达到 O(n)O(n) 的时间复杂度。
算法
这个优化算法与暴力算法仅有两处不同:这些数字用一个 HashSet 保存(或者用 Python 里的 Set),实现 O(1)O(1) 时间的查询,同时,我们只对 当前数字 - 1 不在哈希表里的数字,作为连续序列的第一个数字去找对应的最长序列,这是因为其他数字一定已经出现在了某个序列里。
class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> num_set = new HashSet<Integer>();
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) 个数字。除此以外,所需空间与暴力解法一致。