最长连续系列的解题思路梳理
一、常规的思路
解这道题,我们最常想到的思路肯定是暴力匹配的方法进行匹配,具体是这样的,假设给定的数组为x=[4,6,9,5,8,1,4,3].
- 该思路的代码如下:
int longestConsecutive(vector<int>& nums)
{
int longestStreak = 0;
int n=nums.size();
for (int i = 0; i < n; i++)
{
int currentNum = nums[i];
int currentStreak = 1;
// 内层循环,检查连续的数是否存在
for (int j = 0; j < n; j++)
{
int nextNum = currentNum + 1;
int found = 0;
for (int k = 0; k < n; k++)
{
if (nums[k] == nextNum)
{
found = 1;
break;
}
}
if (found)
{
currentNum = nextNum;
currentStreak++;
} else
{
break;
}
}
if (currentStreak > longestStreak)
{
longestStreak = currentStreak;
}
}
return longestStreak;
}
上面的思路中,有三层的循环,第一层是为了遍历数组中的每一个元素,第二层循环的作用只是为了让他记住当前匹配的值,第三层循环是去寻找数组中是否存在与匹配值相等的元素,解释如下图所示:
上面的举例可知,第二层循环只是为了记住每次的匹配值,那么为什么说这种思路最终其时间复杂度会是O(n2)呢? 假如输入的序列是一个连续序列时,例如x=[1,3,5,7,9],第一层循环由于是要为数组中的每个元素找到以此为起点的最长序列,因此它不管怎样都是要遍历数组中的每一个元素的,因此第一层循环的时间复杂度是O(n);那么第三层循环,此时由于数组是完全不连续的,因此每一个元素要匹配一个nextNum,第三层循环都要执行n次,所以时间复杂度为O(n2),不满足题目要求.
- 那么上面的思路很容易想到的一个改进点,就是最内层的循环不要直接用数组去匹配,因为要看一个数是否存在于数组中,其时间复杂度必定是O(n),因此就想到了用unordered_set来存储数组的元素(因为unordered_set查找一个元素的时间复杂度是O(1)),代码如下:
unordered_set<int> nums_set(x.begin(), x.end()); // 将数组元素插入哈希表
int longestStreak = 0;
for (const int& num : nums_set)
{ // 遍历哈希表中的每个元素
if (!nums_set.count(num - 1))
{ // 只从连续序列的起点开始
int currentNum = num;
int currentStreak = 1;
while (nums_set.count(currentNum + 1))
{ // 在哈希表中查找
currentNum += 1;
currentStreak++;
}
longestStreak = max(longestStreak, currentStreak);
}
}
return longestStreak;
但是,如果仅仅是用unordered_set来代替数组查找,还是会出现最坏情况下时间复杂度为O(n2),原因如下:假设传入数组中的所有元素都是连续的,即:x=[1,2,…,n];那么
- 外层循环:遍历数组中的每一个数x[i] (i=1,2,…,n),其时间复杂度是O(n);
- 内存循环:对于每个数x[i],使用哈希表检查x[i]+1、x[i]+2,…,是否存在;
(1)对于第一个数‘1’,内存循环检查‘2,3,…,n’是否存在,执行n-1次查找操作;
(2)对于第一个数‘2’,内存循环检查‘3,4,…,n’是否存在,执行n-2次查找操作;
(3)依此类推,直到最后一个数 n,内层循环执行 0 次查找操作.
虽然每次查找操作是 O(1) 的,但是由于每个数都会参与到内层循环中,内层循环的总查找操作次数是:
因此最坏情况下时间复杂度仍然是O(n2)
二、正确思路
由上面的分析可知,无论是暴力解法还是使用哈希表来改进元素查找的不变,都会存在一个问题:那就是很多的匹配是不必要的;比如说输入的序列为nums=[2,4,1,3,7],由于第一个数2的前驱1是存在的,那就说明以2为序列的连续序列肯定不是最长的,因此就没有必要去为2这个元素匹配2+1,2+2,2+3…等;只有当前这个数的前驱不存在时,以这个数为起点的序列才有可能是最长序列,所以搞懂了这个,我们就知道了如何去设计一个时间复杂度为O(n)的算法,具体代码如下:
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
//若给定的数组只用一个元素
if(nums.size()<=1)
{
return nums.size();
}
unordered_set<int> nums_set;
//先对nums中的元素去重
for(auto i=nums.begin();i!=nums.end();i++)
{
nums_set.insert(*i);
}
int longestStreak=0; //返回找到的最长序列的长度
for(const int& num:nums_set) //部分for循环
{
//看num是否有前驱,若没有前驱,则:
if(!nums_set.count(num-1))
{
int currentNum=num;
int currentStreak=1;
while (nums_set.count(currentNum+1))
{
currentNum+=1;
currentStreak++;
}
longestStreak=max(longestStreak,currentStreak);
}
}
return longestStreak;
}
};
三、总结
这道题如果没有时间复杂度的限制是非常简单的一道题,用暴力的解法既可轻松解出;从这道题我们可以学习到一种思路:如果一个数的前驱存在于数组中,那么以这个数为起点的连续序列肯定不是最长的连续系列,这也是解这道题的一个关键思路.
unordered_set这个容器是只存储键的,不是存储键值对,并且在插入元素的过程它可以自动去掉重复的元素.
从暴力解法中我们也可以知道一种思路:如果要在一个数组中为每个元素x匹配x+1,x+2,…,那么我们可以加多一层循环来保留当前的匹配值+1(即是:nextNum),只有找到了当前的匹配值,才会更新第二层循环中nextNum,然后进行下一个nextNum的匹配.
break和coutinue语句怎么使用还是要掌握牢固的.