[LeetCode 128] - 最长连续序列(Longest Consecutive Sequence)
问题
给出一个未排序的整数数组,找出最长的连续元素序列的长度。
如:
给出[100, 4, 200, 1, 3, 2],
最长的连续元素序列是[1, 2, 3, 4]。返回它的长度:4。
你的算法必须有O(n)的时间复杂度。
初始思路
要找连续的元素,第一反应一般是先把数组排序。但悲剧的是题目中明确要求了O(n)的时间复杂度,要做一次排序,是不能达到的。不过我们还是先来看看排序的方案要怎么实现。
简单来说,排序后我们只要遍历数组,检查当前值减1是否等于上一个值,如果等,增加当前连续序列的计数;如果不等,将当前计数与当前最大值比较,如果更优替换最大值, 并重置计数为1。具体到细节上,我们还要考虑几个问题:
- 第一个数
处理第一个数时是没有上一个值的。有人可能觉得可以给存储上一个值的变量赋一个特别的初始值来表示处理的是第一个数。但是由于数组内元素的取值范围是所有的整数,不可能找出一个特别的值。所以代码中需要对第一个数做特殊的判断
- 重复的数
数组中可能会有重复的数,所以我们不能光判断当前值减1等于或不等于上一个值。还需要加上一个等不等与上一个值的判断,如果等,说明是一个重复的数字,直接不做任何处理跳到数组中的下一个数。
- 最后一组连续序列
由于我们只在遍历中发现当前值减1不等于上一个值时才尝试更新序列长度最大值。如果有一个连续序列一直持续到数组中的最后一个元素,这个序列的长度是没有得到处理的。因此我们需要在遍历完数组后再尝试更新依稀最大值。
加入了这些细节考虑后,代码就呼之欲出了:
1 class SolutionSort 2 { 3 public: 4 int longestConsecutive(std::vector<int> &num) 5 { 6 std::sort(num.begin(), num.end()); 7 8 int maxLen = 0; 9 int currentLen = 0; 10 int previous = 0; 11 12 for(auto iter = num.begin(); iter != num.end(); ++iter) 13 { 14 if(iter == num.begin())//第一个数的特殊情况 15 { 16 ++currentLen; 17 } 18 else 19 { 20 if(*iter == previous)//重复数的情况 21 { 22 continue; 23 } 24 else if(*iter - 1 == previous) 25 { 26 ++currentLen; 27 } 28 else 29 { 30 if(currentLen > maxLen) 31 { 32 maxLen = currentLen; 33 } 34 currentLen = 1; 35 } 36 } 37 previous = *iter; 38 } 39 40 //有一个连续序列持续到数组最后一个元素的情况 41 if(currentLen > maxLen) 42 { 43 maxLen = currentLen; 44 } 45 46 return maxLen; 47 } 48 };
提交后发现其实是可以通过Judge Small和Judge Large的。但是这种方案始终不符合题目要求。
优化
要实现O(n)的时间复杂度,就不能对数组排序。其实我们大脑在判断这个问题时就是不排序的。让我们来模拟一下:
你看到[100, 4, 200, 1, 3, 2]这个数组,首先你会看99或者101在不在这个数组里,发现数组没这两个数,那么100组成的连续序列长度仅为1。接着会看5或者3在不在数组里,会发现3存在,5不存在;紧接着会看2在不在....直到发现0不在。从而得到4组成的最长序列为4。
总结一下会发现,我们在判断某个数的连续序列时,会分别往减小和增大的方向找下一个连续数在不在数组中。然后把两个方向的长度加起来即为包含该数的一个连续序列。需要注意的是,当前数的长度计数只需要出现在一个方向的查找中计算即可,否则就重复了。要找一个数是不是在数组中,不可能用遍历的方法实现,这样时间复杂度就超过O(n)了。而要降低时间复杂度,一个经典的方案就是空间换时间。用增加空间复杂度的方法来换取时间复杂度的降低。所以我们可以先对数组进行一次预处理,生成一份包含数组元素的哈希表。这样在求解某个数字在不在数组时就可以得到O(1)的时间复杂度。
那么我们可以得到如下伪代码:
找连续序列函数(要找序列的值,方向)
循环直到要找的值不在哈希表中
序列长度+1
如果增加方向,要找的序列值+1
如果减少方向,要找的序列值-1
循环结束
返回序列长度
找连续序列函数结束
求解函数(数组)
遍历数组生成哈希表
遍历数组
序列长度1 = 找连续序列函数(当前值,增加方向)
序列长度2 = 找连续序列函数(当前值 - 1,减少方向)
如果序列长度1 + 序列长度2 > 当前最长序列,更新最长序列
遍历结束
求解函数结束
这个方案的时间复杂度应该是O(n) + O(n) * O(1) * O(平均序列长度)。如果平均序列长度等于n,如数组[3,4,2,1],复杂度就是O(n^2)了。看来还不可行,主要的时间复杂度都浪费在找连续序列上了。怎么能减少找连续序列的时间复杂度?经过观察我们可以发现,4的最长序列和3,2,1的最长序列其实是一样的。找过了4之后其实后面这3个数都不用找了。而我们控制是否查找一个数的连续序列是通过判断数字是否在哈希表中来实现的,也就是说,如果我们可以在找出一个数字在连续序列中后就将其移除,就可以避免以后再触发查找的循环。通过这个优化,时间复杂度将变为O(n) + O(n) + O(序列长度总和),可以认为是O(n)了。最后得出代码如下:
1 classSolution 2 { 3 public: 4 int longestConsecutive(std::vector<int> &num) 5 { 6 for(int i = 0; i < num.size(); ++i) 7 { 8 flags_.insert(num[i]); 9 } 10 11 int maxLen = 0; 12 13 for(int i = 0; i < num.size(); ++i) 14 { 15 int ascendingMax = FindConsecutiveNumbers(ASCENDING, num[i]); 16 int decendingMax = FindConsecutiveNumbers(DECENDING, num[i] - 1); 17 18 19 if(ascendingMax + decendingMax > maxLen) 20 { 21 maxLen = ascendingMax + decendingMax; 22 } 23 } 24 25 return maxLen; 26 } 27 28 private: 29 enum OrderBy 30 { 31 ASCENDING, 32 DECENDING 33 }; 34 35 int FindConsecutiveNumbers(OrderBy orderBy, int value) 36 { 37 int maxLen = 0; 38 39 while(flags_.find(value) != flags_.end()) 40 { 41 ++maxLen; 42 43 flags_.erase(value); 44 45 if(orderBy == ASCENDING) 46 { 47 ++value; 48 } 49 else 50 { 51 --value; 52 } 53 } 54 55 return maxLen; 56 } 57 58 std::unordered_set<int> flags_; 59 };