LeetCode 热题 100——哈希——最长连续序列
3. 最长连续序列
题目描述
给定一个未排序的整数数组 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
求解
(1)去重 + 排序 + 遍历
var longestConsecutive = function(nums) {if (nums.length === 0) return 0// 思路: 排序 + 遍历// 一开始没考虑到重复序列,对于重复序列 [1,2,2,3] -> 3,排序的时候要去重nums.sort((a, b) => a - b); nums = [...new Set(nums)].sort((a, b) => a - b)let count = 0let max_count = 1 // 至少为1for(let i = 0; i < nums.length - 1; i++) {if(nums[i] + 1 === nums[i + 1]) {count++max_count = Math.max(max_count, count + 1)} else {count = 0}}return max_count
};
真的是习惯了复制粘贴的世界,自己敲代码百般漏洞(哭死);之前学的也全忘了,真的是每一处都是细节,虽然有点思路,但写出来s一样。
上面是自己的思路,也可用哈希求解
(2)使用Set
var longestConsecutive = function(nums) {if (nums.length === 0) return 0;let set = new Set(nums) // 去重let ans = 1 // 至少有一个for (let item of set) {// 找最小值if(!set.has(item - 1)) {// 满足条件,则为 该数组中 某一连续序列的最小值let count = 0 // 计数器let cur = item // 记录之后的连续值while(set.has(cur + 1)) {// while(set.has(cur)) {count++cur++}// while循环结束,说明 某一段连续序列已经遍历完成ans = Math.max(ans, count + 1)// ans = Math.max(ans, count)}}return ans
}
(3)两种解法的时间复杂度分析:
- 排序 + 遍历(O (N log N))
- sort() 排序的时间复杂度是 O(N log N)(JavaScript 的 sort 用 Timsort 算法);
- 后续遍历数组是 O(N),但整体被排序的 O(N log N) 主导。
- 为什么慢:排序本身就是一个耗时的操作,尤其是数据量大的时候。
- Set 查找(O (N))
- 用 Set 存储数据,has() 查找是 O(1)(哈希表特性)。
- 外层 for 循环遍历所有元素(O (N)),但内层 while 循环不会重复遍历同一个连续序列(例如,找到 1 后,会一次性遍历 1,2,3,4,后续再遇到 2,3,4 时会直接跳过)。
- 整体下来,所有元素只会被遍历一次,总时间复杂度 O(N)。
- 为什么快:用空间换时间,避免了排序,改用哈希表快速查找来确定连续序列。
补充知识点
sort()
Array.prototype.sort() 默认按 “字符串 Unicode 编码顺序” 排序,而非数字大小。它会先将数组中所有元素转换为字符串,再逐个比较字符的 Unicode 编码值:
- 转换后字符串为
["100", "4", "200", "1", "3", "2"] - 比较规则:先比第一个字符的编码(“1” < “2” < “3” < “4”),第一个字符相同时再比后续字符(如 “100” 与 “1”,第一个字符都是 “1”,但 “1” 长度更短,所以排在前面)。
因此,排序后的顺序是按字符串编码排列的,而非数字逻辑上的大小顺序。
-
如何实现数字升序 / 降序?
- 需手动传入比较函数 (a, b) => a - b(升序)或 (a, b) => b - a(降序):
// 数字升序(正确排序) nums.sort((a, b) => a - b); // 结果:[1, 2, 3, 4, 100, 200]// 数字降序 nums.sort((a, b) => b - a); // 结果:[200, 100, 4, 3, 2, 1]
去重 + 排序
-
Set 去重 + 排序(推荐)
const nums = [100, 4, 200, 1, 3, 2, 2, 100];// 一行实现:去重 + 数字升序 const result = [...new Set(nums)].sort((a, b) => a - b);console.log(result); // [1, 2, 3, 4, 100, 200]核心简化点:
用扩展运算符 […new Set(nums)] 直接将 Set 转为数组(替代 Array.from),代码更紧凑;
排序时直接传入数字比较函数,无需额外变量。 -
filter 去重 + 排序
const nums = [100, 4, 200, 1, 3, 2, 2, 100];// 简化为一行 filter + 排序 const result = nums.filter((v, i, self) => self.indexOf(v) === i).sort((a, b) => a - b);console.log(result); // [1, 2, 3, 4, 100, 200]去掉中间变量,直接链式调用 filter 和 sort; 箭头函数简化 filter 的回调逻辑。
-
Map 去重 + 排序(适合复杂场景)
const nums = [100, 4, 200, 1, 3, 2, 2, 100];// 简化 Map 去重逻辑,一行转换为数组 const result = Array.from(new Map(nums.map(v => [v, true])).keys()).sort((a, b) => a - b);console.log(result); // [1, 2, 3, 4, 100, 200]用 nums.map(v => [v, true]) 直接生成 Map 的键值对数组,替代 forEach 循环;
链式调用 Array.from 和 sort,缩减代码行数。
总结:日常开发首选:[…new Set(nums)].sort((a, b) => a - b)(最简洁、性能最优)
== 和 ===
核心区别在于:是否进行类型转换。
-
=== 严格相等 (Strict Equality)
比较规则:
先比较两边的值的 类型,如果类型不同,直接返回 false。
如果类型相同,再比较它们的 值 是否相等。
优点:行为可预测,不会产生意想不到的结果,是推荐的比较方式。
示例:console.log(5 === 5); // true (类型相同,值相同) console.log('5' === 5); // false (类型不同:字符串 vs 数字) console.log(true === 1); // false (类型不同:布尔值 vs 数字) console.log(null === undefined); // false (类型不同:null vs undefined) console.log([] === []); // false (类型相同,但引用的是不同的数组对象) -
== 宽松相等 (Loose Equality)
比较规则:
先检查两边的值的类型,如果类型相同,就和 === 一样比较值。
如果 类型不同,它会尝试进行 类型转换,将两边的值转换为同一个类型后再进行比较。
缺点:类型转换的规则非常复杂,容易导致难以理解和调试的错误。
示例:console.log(5 == 5); // true (类型相同,值相同) console.log('5' == 5); // true (字符串 '5' 被转换为数字 5) console.log(true == 1); // true (布尔值 true 被转换为数字 1) console.log(null == undefined); // true (这是一个特例,它们互相等于) console.log([] == ''); // true (数组 [] 转换为字符串是 '', 所以 '' == '' 为 true) console.log([] == 0); // true (数组 [] -> 字符串 '' -> 数字 0) console.log('' == 0); // true (字符串 '' 转换为数字 0)可以看到,== 的比较结果有时会让人感到困惑。
-
核心区别对照表
| 特性 | === 严格相等 | == 宽松相等 |
|---|---|---|
| 类型检查 | 先比较类型,类型不同则为 false | 类型不同时,会尝试将值转换为同一类型 |
| 结果可预测性 | 高,行为清晰 | 低,转换规则复杂,易出错 |
| 推荐使用场景 | 几乎所有场景 | 仅在某些特定、明确需要利用其类型转换规则的情况下使用 |
| null vs undefined | null === undefined 为 false | null == undefined 为 true (唯一特例) |
Set
Set 是一种内置对象,用于存储唯一值的集合(即集合中的元素不会重复)。它的核心特点和用法如下:
1.基本概念
Set 是 ES6 新增的结构,以值 - 值的形式存储数据(与 Map 的 “键 - 值” 不同),且元素唯一(重复添加同一值会被自动忽略)。
2.常用方法
| 方法 | 功能描述 |
|---|---|
| new Set() | 创建一个空的 Set 实例。 |
| set.add(value) | 向 Set 中添加一个值,返回 Set 本身(可链式调用)。 |
| set.delete(value) | 删除指定值,返回布尔值(成功删除为 true,否则 false)。 |
| set.has(value) | 判断 Set 中是否存在指定值,返回布尔值(true/false)。 |
| set.clear() | 清空 Set 中所有值。 |
| set.size | 只读属性,返回 Set 中值的数量。 |
3.遍历方式
Set 是可迭代对象,支持以下遍历方式:
for...of遍历:for (let value of set) { … }set.keys():遍历所有值(与 set.values() 效果一致,因为 Set 是 “值 - 值” 结构)。set.entries():遍历所有值的 “键值对”(但键和值相同,如 [1, 1])。
4.与数组的区别
| 特性 | Set | 数组(Array) |
|---|---|---|
| 元素唯一性 | 元素唯一(重复值会被忽略) | 允许重复元素 |
| 遍历便利性 | 原生支持迭代(for…of) | 需手动遍历或用 forEach |
| 去重操作 | 天生去重,直接创建 Set 即可 | 需借助 filter 或 Set 转换(如 […new Set(arr)]) |
5.代码示例
// 创建 Set
const set = new Set();// 添加值(可链式调用)
set.add(1).add(2).add(2); // 重复的 2 会被忽略
console.log(set); // Set { 1, 2 }// 检查是否存在值
console.log(set.has(2)); // true
console.log(set.has(3)); // false// 删除值
set.delete(1);
console.log(set); // Set { 2 }// 遍历
for (let value of set) {console.log(value); // 2
}// 转换为数组(常用去重方式)
const arr = [1, 2, 2, 3];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // [1, 2, 3]
6.典型应用场景
- 数组去重:
[...new Set(arr)]是最简洁的数组去重方式。 - 判断元素是否重复:利用 Set 的唯一性,可快速检查数据中是否存在重复项。
- 交集、并集、差集计算:结合 Set 的遍历和方法,可高效实现集合运算(如
const intersection = new Set([...set1].filter(x => set2.has(x))))。
