set和map

很多新手都会混淆 SetMap,也会疑惑「为什么它们的查询是 O (1)」------ 我先帮你理清「Set vs Map」的关系,再用通俗的方式解释「为什么时间复杂度是 O (1)」,全程结合前端场景,保证你能分清、记牢。

一、先搞懂:Set 和 Map 都是「哈希表」,只是用途不同

你昨天学的 Map 是哈希表,今天遇到的 Set 同样是哈希表(底层实现逻辑完全一样),只是两者的「存储形式」不同 ------ 可以把它们理解成「哈希表的两种不同用法」:

特性 Map(映射 / 字典) Set(集合)
存储形式 键值对(key: value) 只有值(value,且值唯一)
核心作用 「映射」(通过 key 找 value) 「去重 + 快速查询」(判断值是否存在)
常用方法 set(key, val) get(key) has(key) add(val) has(val) delete(val)
类比生活例子 通讯录(名字→手机号) 花名册(只记录名字,不重复)
算法题常用场景 两数之和(存数→下标)、字母异位词分组(存特征键→数组) 最长连续序列(去重 + 查数是否存在)、数组去重
用「抽屉」比喻两者的区别(底层都是哈希表)
  • 哈希表的底层 = 一排带编号的抽屉(通过哈希函数计算值 / 键对应的抽屉号);
  • Map:每个抽屉里放「一个信封」,信封上写 key,里面装 value(比如抽屉 7 放「7:1」);
  • Set:每个抽屉里只放「一张纸条」,纸条上写值(且每个抽屉最多放一张,不重复),比如抽屉 7 放「7」,再放 7 就会被忽略。
前端算法题里的选择原则(新手直接记)
  • 需要「键值对应」(比如存数的下标、存分组数组)→ 用 Map
  • 只需要「去重」或「快速判断值是否存在」→ 用 Set(更简洁)。
代码示例:Set 的基础用法(对应最长连续序列)

javascript

运行

复制代码
// 1. 初始化空Set
const numSet = new Set();
// 2. 添加值(自动去重)
numSet.add(1);
numSet.add(2);
numSet.add(2); // 重复值,不会添加
// 3. 判断值是否存在(O(1))
console.log(numSet.has(1)); // true
console.log(numSet.has(3)); // false
// 4. 遍历Set
for (const num of numSet) {
  console.log(num); // 1、2(去重后的结果)
}

二、为什么 Set/Map 的 has 方法是 O (1)?(核心)

你可能会问:「Set 里存了一堆数,找某个数为什么不用遍历?」------ 答案和你之前学的 Map 一样:Set/Map 的底层是哈希表,查询时通过「哈希函数」直接定位值的存储位置,不是遍历

再用「快递柜」比喻(简化版底层逻辑)
  1. 存值时
    • 比如往 Set 里加数字 100,哈希函数会计算 100 的哈希值(比如 100 % 10 = 0);
    • 直接把 100 放进「0 号快递柜」,不用管其他柜子。
  2. 查询时numSet.has(100)):
    • 先用同一个哈希函数计算 100 的哈希值 → 0 号柜;
    • 直接打开 0 号柜,看里面有没有 100 → 有就返回 true,没有就返回 false;
    • 整个过程只有「计算哈希值 + 开柜」两步,和 Set 里有多少个数无关(哪怕有 1000 个数,找 100 还是只查 0 号柜)。
对比:数组的 includes 是 O (n),Set 的 has 是 O (1)

用 10 万条数据做对比,你能直观看到差异:

javascript

运行

复制代码
// 1. 准备10万个数字
const nums = [];
for (let i = 0; i < 100000; i++) {
  nums.push(i);
}
const numSet = new Set(nums);

// 2. 数组查询(includes):遍历找99999,O(n)
console.time('数组查询');
nums.includes(99999);
console.timeEnd('数组查询'); // 耗时约5ms(随数据量增长)

// 3. Set查询(has):直接定位,O(1)
console.time('Set查询');
numSet.has(99999);
console.timeEnd('Set查询'); // 耗时约0.01ms(几乎不随数据量增长)
关键结论:
  • 数组的 includes:要从第一个数开始遍历,直到找到目标值(最坏遍历整个数组)→ O (n);
  • Set/Map 的 has:通过哈希函数直接定位存储位置,一步到位 → O (1)。

三、回到「最长连续序列」,理解 Set 的作用

这道题用 Set 而不用 Map,核心原因是:

  1. 去重 :比如数组 [0,0,1],去重后只需要处理 0、1,避免重复计算序列长度;
  2. 快速查询 :判断 num-1/num+1 是否存在时,用 numSet.has()(O (1)),如果用数组的 includes 会变成 O (n²),不符合题目「O (n) 时间复杂度」的要求。
反例:不用 Set,用数组的后果

javascript

运行

复制代码
var longestConsecutive = function(nums) {
  if (nums.length === 0) return 0;
  let maxLength = 1;
  // 不用Set,直接遍历数组(去重都没做)
  for (const num of nums) {
    if (!nums.includes(num - 1)) { // includes是O(n)
      let currentNum = num;
      let currentLength = 1;
      while (nums.includes(currentNum + 1)) { // includes是O(n)
        currentNum++;
        currentLength++;
      }
      maxLength = Math.max(maxLength, currentLength);
    }
  }
  return maxLength;
};
// 输入10万条数据时,这个代码会卡死(O(n²)复杂度),而用Set的版本能快速运行

总结

  1. Set 和 Map 的关系:都是哈希表,Map 存「键值对」(映射),Set 存「唯一值」(去重 + 查询),底层查询都是 O (1);
  2. O (1) 的原因:通过哈希函数直接定位存储位置,不用遍历,查询时间和数据量无关;
  3. 最长连续序列用 Set 的原因:去重 + O (1) 快速判断数是否存在,保证整体时间复杂度是 O (n)。

现在你能分清 Set 和 Map 了,也懂了为什么它们的查询是 O (1)------ 如果还有哪个细节没懂(比如 Set 的 add 方法、哈希函数的具体计算),都可以问,我会拆到最基础的层面~

set为什么每个只存一个,怎么计算的比如7和17/27

你问的这两个问题,刚好是理解 Set 核心特性的关键 ------「为什么 Set 只存一个重复值」「哈希冲突时 Set 怎么处理(比如 7、17、27)」,我用「大白话 + 底层逻辑 + 示例」帮你彻底讲透,还是结合你熟悉的「快递柜」比喻,保证好懂。

一、先懂:Set 为什么「每个值只存一个」(去重原理)

Set 的核心定义就是「值的集合,且所有值唯一,重复添加会被忽略」------ 这不是复杂的计算,而是 Set 的「内置规则」,底层实现时会先判断值是否已存在,再决定是否添加。

1. Set 添加值的完整流程(以添加 7 为例)

当你执行 set.add(7) 时,Set 会做两步操作:① 用哈希函数计算 7 的哈希值(比如 7 % 10 = 7),找到对应的存储位置(7 号快递柜);② 检查 7 号柜里是否已有「7」这个值:

  • 如果没有 → 把 7 存进去;
  • 如果已有 → 直接忽略,不做任何操作。
2. 代码验证:重复添加会被忽略

javascript

运行

复制代码
const set = new Set();
set.add(7); // 7号柜空,存入7
set.add(7); // 7号柜已有7,忽略
set.add(17); // 哈希值17%10=7,7号柜没有17,存入17
console.log(set); // 输出 Set {7, 17}(没有重复的7)
3. 核心结论:

Set 的「去重」不是靠「计算」,而是靠「添加前的存在性检查」------ 每次 add 值时,都会先通过哈希表的 O (1) 查询判断值是否已存在,存在就跳过,不存在才存入,所以最终 Set 里不会有重复值。

二、再懂:7、17、27 共存时,Set 怎么计算 / 存储(哈希冲突处理)

你问的 7、17、27,按「哈希函数 = 值 %10」计算,哈希值都是 7(7%10=7、17%10=7、27%10=7)------ 这就是「哈希冲突」(多个值映射到同一个存储位置),但 Set 依然能存下这三个值,核心靠「链地址法」(和 Map 的冲突处理完全一样)。

1. 用「快递柜 + 层板」比喻冲突处理
  • 哈希表的存储位置 = 快递柜(编号 0-9);
  • 每个快递柜里不是只放一张纸条,而是有「层板 / 小格子」(链表结构),每个层板可以放一个唯一值;
  • 冲突的值会按顺序存在同一个柜子的不同层板里。
2. 7、17、27 存入 Set 的完整过程
操作 哈希函数计算 存储位置(柜号) 柜子内状态(层板) Set 最终内容
set.add(7) 7%10=7 7 号柜 层板 1:7 {7}
set.add(17) 17%10=7 7 号柜 层板 1:7 → 层板 2:17 {7,17}
set.add(27) 27%10=7 7 号柜 层板 1:7 → 层板 2:17 → 层板 3:27 {7,17,27}
3. 查询时怎么找到目标值(比如查 17 是否存在)

当执行 set.has(17) 时:① 计算 17 的哈希值 → 7%10=7,定位到 7 号柜;② 遍历 7 号柜里的层板(从层板 1 开始):

  • 层板 1:7 → 不是 17,继续;
  • 层板 2:17 → 找到,返回 true;③ 如果遍历完所有层板都没找到,返回 false。
4. 为什么还是 O (1)?

虽然冲突时需要遍历柜子里的层板,但哈希函数的「均匀性」会保证:

  • 大多数柜子里只有 1-2 个层板(比如 10 万个值分布在 100 个柜子里,每个柜子平均 1000 个,但实际 JS 引擎的哈希函数更优,冲突极少);
  • 遍历少量层板的时间可以忽略,所以查询 / 添加的平均时间复杂度依然是 O (1)。

三、补充:JS 里 Set 判断「值是否重复」的规则

Set 判断两个值是否相同,不是「看哈希值」,而是用「SameValueZero」算法(简单说就是严格相等===,但特殊处理 NaN):

javascript

运行

复制代码
const set = new Set();
// 1. 数字7和字符串"7":哈希值可能相同,但Set认为不同
set.add(7);
set.add("7");
console.log(set); // Set {7, "7"}

// 2. NaN和NaN:严格相等===认为不同,但Set认为相同(特殊处理)
set.add(NaN);
set.add(NaN);
console.log(set); // Set {7, "7", NaN}

// 3. 对象:即使内容相同,也认为不同(引用类型,哈希值不同)
set.add({a:1});
set.add({a:1});
console.log(set); // Set {7, "7", NaN, {a:1}, {a:1}}

总结

  1. Set 去重原因:添加值时会先通过哈希表查询值是否已存在,存在则忽略,核心是「存在性检查」而非复杂计算;
  2. 7/17/27 的存储:哈希值相同(冲突),Set 用「链地址法」把它们存在同一个存储位置的不同链表节点(层板)里,查询时遍历该位置的少量节点即可;
  3. 时间复杂度:冲突时的小遍历不影响整体 O (1),因为哈希函数保证冲突极少。
相关推荐
项目申报小狂人2 小时前
CEC2024获胜算法mLSHADE-RL:具有重新启动和本地搜索机制的多操作员集成LSHADE
算法
Faker66363aaa2 小时前
YOLOv26人脸检测算法实现与优化_2
算法·yolo
格林威2 小时前
Baumer相机铁轨表面裂纹巡检:提升铁路安全监测能力的 7 个关键技术,附 OpenCV+Halcon 实战代码!
人工智能·数码相机·opencv·算法·安全·计算机视觉·分类
m0_686041612 小时前
C++中的装饰器模式变体
开发语言·c++·算法
web小白成长日记2 小时前
从一道面试题看算法思维:最小栈(Min Stack)的从 O(N) 到 O(1) 进化之路
算法
Remember_9932 小时前
网络编程套接字深度解析:从理论到实践的完整指南
网络·算法·http·https·udp·哈希算法·p2p
钮钴禄·爱因斯晨2 小时前
机器学习(三):聚焦KNN算法距离度量、特征预处理与超参数选择
人工智能·算法·机器学习
星火开发设计2 小时前
动态内存分配:new 与 delete 的基本用法
开发语言·c++·算法·内存·delete·知识·new
CDA数据分析师干货分享2 小时前
【CDA干货】客户分群建模——RFM+K-Means用户画像——电商用户数据分析全流程:从数据到增长决策
算法·机器学习·数据挖掘·数据分析·kmeans·cda证书