当面对LeetCode第三题"最长连续序列"时,我们首先需要彻底理解题目要求。给定一个未排序的整数数组,我们需要找出数字连续的最长序列的长度。这里的"连续"指的是数字值连续(如1,2,3),而不是在数组中的位置连续。
举个例子,对于数组[100,4,200,1,3,2],最长的连续序列是[1,2,3,4],长度为4。注意序列中的元素在原数组中可以是分散的,且数组可能包含重复元素。
这个问题的难点在于如何在高效的时间复杂度内完成查找。最直观的暴力解法是对每个数字,检查其+1、+2...是否存在,这样时间复杂度会达到O(n²),显然不符合题目对效率的要求。
另一个需要考虑的是重复元素的处理。如果数组中有多个相同的数字,它们不应该影响最终的结果。例如[1,2,2,3]的最长连续序列长度仍然是3(1,2,3)。
基于上述分析,我们需要一个能够:
哈希表(在C++中是unordered_set)完美满足这些需求。它提供了O(1)时间复杂度的查找能力,同时自动去重。
我们选择unordered_set而非unordered_map,因为我们只需要存储键(数字本身),不需要存储值。unordered_set的底层实现是哈希表,提供了常数时间的查找效率。
cpp复制unordered_set<int> hash;
hash.reserve(nums.size()); // 预分配内存
for(int& num : nums) {
hash.insert(num); // 自动去重
}
这里使用了reserve()预分配内存,这是一个重要的优化点。如果不预分配,当元素数量增加导致哈希表扩容时,会触发rehash操作,严重影响性能。
关键思路是:只从序列的起点开始查找。如何判断一个数字是否是序列起点?如果num-1不在哈希表中,那么num就是一个序列起点。
cpp复制int longest_count = 0;
for(const int& num : hash) {
if(!hash.count(num - 1)) { // 是序列起点
int current_num = num;
int current_count = 1;
while(hash.count(current_num + 1)) {
current_num++;
current_count++;
}
longest_count = max(longest_count, current_count);
}
}
这种策略确保每个数字最多被访问两次:一次是作为序列起点被检查,一次是作为序列成员被计数。因此整体时间复杂度是O(n)。
for(const int& num : hash)而非值传递,避免不必要的拷贝。虽然代码中有嵌套循环(for+while),但由于我们只从序列起点开始查找,每个数字最多被访问两次:
因此,总时间复杂度是O(n)。
我们使用了一个unordered_set存储所有元素,最坏情况下(无重复元素)需要O(n)的额外空间。
需要考虑的特殊情况包括:
unordered_set的底层是哈希表,由以下部分组成:
理解这些有助于我们优化性能。比如预分配足够大的桶数量可以避免rehash。
在遍历哈希表时,需要注意迭代器失效的情况。虽然我们的算法不会修改哈希表,但了解这一点对编写安全的C++代码很重要。unordered_set的插入操作可能导致所有迭代器失效(如果触发rehash)。
unordered_set使用链地址法解决哈希冲突。当多个key映射到同一个桶时,它们会被存储在链表中。这解释了为什么最坏情况下查找时间复杂度会退化到O(n)。
如前所述,使用reserve()预分配内存可以避免rehash:
cpp复制hash.reserve(nums.size());
即使有重复元素,预分配nums.size()也是安全的,因为实际存储的元素不会超过这个数量。
使用const引用遍历比值传递更高效:
cpp复制for(const int& num : hash) // 推荐
for(int num : hash) // 不推荐,会有拷贝开销
另一种思路是先排序再查找连续序列。虽然排序的O(nlogn)时间复杂度理论上比我们的O(n)解法差,但对于某些特定情况(如数据量不大但哈希冲突严重)可能实际更快。
可能原因:
unordered_set会自动处理重复元素,这是选择它的主要原因之一。如果使用其他数据结构,需要手动去重。
如果不这样做,会导致重复计算。例如对于序列[1,2,3,4],如果从2开始查找,会得到[2,3,4],但这已经被包含在从1开始的查找结果中了。
这个问题有一些有趣的变种:
这种算法在实际中有多种应用:
虽然我们使用C++实现,但了解其他语言的实现方式也很有帮助:
Python示例:
python复制def longestConsecutive(nums):
num_set = set(nums)
longest = 0
for num in num_set:
if num - 1 not in num_set:
current_num = num
current_streak = 1
while current_num + 1 in num_set:
current_num += 1
current_streak += 1
longest = max(longest, current_streak)
return longest
Java示例:
java复制public int longestConsecutive(int[] nums) {
Set<Integer> num_set = new HashSet<>();
for (int num : nums) {
num_set.add(num);
}
int longest = 0;
for (int num : num_set) {
if (!num_set.contains(num - 1)) {
int current = num;
int currentStreak = 1;
while (num_set.contains(current + 1)) {
current += 1;
currentStreak += 1;
}
longest = Math.max(longest, currentStreak);
}
}
return longest;
}
解决这个问题让我深刻理解了哈希表在算法中的巧妙应用。关键在于:
在实际编码中,我发现预分配内存对性能提升非常明显。另外,理解算法为什么能达到O(n)时间复杂度也很重要,这有助于我们在其他问题中应用类似的思想。
最后,建议在解决这类问题时: