给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n)
的算法解决此问题。
示例 1:
输入:nums = [100,4,200,1,3,2] 输出:4 解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1] 输出:9
示例 3:
输入:nums = [1,0,1,2] 输出:3
提示:
0 <= nums.length <= 105
-109 <= nums[i] <= 109
解题思路
要达到O(n)的时间复杂度,不能使用排序方法(排序是O(n log n))。关键思路是:
- 使用HashSet存储所有数字:这样可以在O(1)时间内查找任意数字是否存在
- 找到每个连续序列的起始点:对于数字num,如果num-1不存在,说明num是某个连续序列的起始点
- 从起始点开始计算序列长度:从起始点开始,依次查找num+1, num+2, num+3...直到找不到为止
- 记录最长序列长度:在所有序列中找到最长的那个
这样做的巧妙之处在于:虽然看起来有嵌套循环,但每个数字最多被访问两次(一次在外层循环判断是否为起始点,一次在内层循环作为某个序列的一部分),所以总时间复杂度仍然是O(n)。
import java.util.*; public class Solution { public int longestConsecutive(int[] nums) { // 第一步:处理边界情况 // 如果数组为空或null,直接返回0 if (nums == null || nums.length == 0) { return 0; } // 第二步:把数组中的所有数字放到HashSet里 // HashSet的作用:可以在O(1)时间内快速查找某个数字是否存在 // 去除重复数字,只关心数字存在与否 Set<Integer> numSet = new HashSet<>(); // for (int num : nums) 是增强for循环的写法 // 意思是:从nums数组中依次取出每个元素,赋值给变量num // 等价于传统的for循环,但更简洁 for (int num : nums) { numSet.add(num); // 把每个数字都加到HashSet里 } // 第三步:准备一个变量记录最长的连续序列长度 int longestStreak = 0; // 第四步:遍历HashSet中的每个数字 // for (int num : numSet) 同样是增强for循环 // 从numSet中依次取出每个数字进行处理 for (int num : numSet) { // 关键判断:这个数字是不是连续序列的"开头"? // 判断方法:看看比它小1的数字存不存在 // 如果num-1不存在,说明num就是某个连续序列的起始点 // 这样做是为了避免重复计算同一个序列 if (!numSet.contains(num - 1)) { // 如果是序列开头,开始计算这个序列的长度 int currentNum = num; // 当前正在检查的数字,从开头数字开始 int currentStreak = 1; // 当前序列的长度,开头数字算1个 // 从开头数字开始,一直往后找连续的数字 // numSet.contains(currentNum + 1) 是检查下一个数字存不存在 // 注意:这里只是检查currentNum + 1,不会改变currentNum的值 while (numSet.contains(currentNum + 1)) { // 如果下一个数字存在,就移动到下一个数字 currentNum++; // 真正移动:把currentNum增加1 currentStreak++; // 序列长度也增加1 // 举例:如果当前是1,检查2存在,就移动到2,长度变成2 // 如果当前是2,检查3存在,就移动到3,长度变成3 // 如果当前是4,检查5不存在,就退出while循环 } // while循环结束后,我们得到了从num开始的完整连续序列长度 // 现在检查这个序列是不是目前最长的 // Math.max(a, b) 返回两个数中较大的那个 // 比较当前记录的最长长度 和 刚找到的序列长度,取较大值 longestStreak = Math.max(longestStreak, currentStreak); // 举例:如果之前最长是1,现在找到长度4的序列,就更新为4 // 如果之前最长是4,现在找到长度2的序列,还是保持4 } // 如果num-1存在,说明num不是序列开头,跳过不处理 // 这样避免了重复计算同一个序列 } // 返回找到的最长连续序列长度 return longestStreak; } } /* 完整示例演示:nums = [100, 4, 200, 1, 3, 2] 1. 建立HashSet: {100, 4, 200, 1, 3, 2} 2. 遍历每个数字: 检查100: - contains(99)? false → 100是开头 - contains(101)? false → 序列[100],长度1 - longestStreak = max(0, 1) = 1 检查4: - contains(3)? true → 4不是开头,跳过 检查200: - contains(199)? false → 200是开头 - contains(201)? false → 序列[200],长度1 - longestStreak = max(1, 1) = 1 检查1: - contains(0)? false → 1是开头 - contains(2)? true → 移动到2,长度2 - contains(3)? true → 移动到3,长度3 - contains(4)? true → 移动到4,长度4 - contains(5)? false → 停止 - 序列[1,2,3,4],长度4 - longestStreak = max(1, 4) = 4 检查3和2: 都不是开头,跳过 3. 返回结果:4 */
今日学到的是
为什么在这个问题中使用 HashSet<Integer> 而不是 HashMap<Integer, Integer>。
核心区别
HashSet:只存储值,用于快速判断元素是否存在
HashMap:存储键值对,用于建立映射关系
在这个问题中的分析
我们的需求
在最长连续序列问题中,我们只需要:
- 快速查找某个数字是否存在于数组中
- 不需要存储任何额外信息