什么是哈希?什么时候必须用哈希?数组、Set、Map 这三种哈希实现有什么区别?每种方法对应哪些算法题?用 JavaScript 代码一步步拆解,看完你会发现,原来 "判断元素是否出现过" 这么简单。
先搞懂:什么是哈希表?它凭什么快?
哈希表(Hash Table)是一种通过键值对存储数据 的数据结构,它的核心是 "哈希函数"------ 能把 "键" 快速映射到对应的 "值" 的存储位置,从而实现快速查找、插入、删除(平均时间复杂度 O (1))。
简单说,哈希表就像一本字典:你想查 "apple" 的意思(值),直接按首字母找到对应页码(哈希函数映射的位置),不用从第一页翻到最后一页(避免遍历)。

在 JS 中,我们不直接实现哈希表,而是用数组、Set、Map这三种内置结构来模拟,它们本质上都是哈希表的 "简化版",只是适用场景不同。
什么时候必须用哈希表?记住这个核心场景
哈希表不是万能的,但遇到以下场景时,它一定是最优解:
核心场景:需要快速判断 "一个元素是否出现过"
比如:
- 检查数组中是否有重复元素;
- 两数之和:找到数组中是否存在 "目标值 - 当前元素" 这个互补元素;
- 统计字符串中每个字符出现的次数。
这些问题如果用嵌套循环(O (n²))会超时,而用哈希表能降到 O (n),效率天差地别
哈希表的 3 种实现方式:数组、Set、Map,该怎么选?
JS 中没有专门的 "哈希表" 对象,但这三种结构能完美替代,关键是根据数据范围 和是否需要存储键值对来选择。
(1)数组:适合数值范围小且连续的场景
适用场景:
- 元素是有限范围的整数(比如 0-255 的 ASCII 码、0-100 的分数);
- 只需用 "索引" 作为键,"值" 记录是否出现(或出现次数)。
原理:
数组的索引本身就是 "哈希函数"------ 索引直接映射到内存地址,访问速度极快。比如用arr[10] = true
表示 "10 这个元素出现过",判断时直接查arr[10]
是否为true
。
例题

解题思路:
- 字母只有 26 个(a-z),范围小且连续,适合用数组计数;
- 遍历 s,给每个字母对应的索引 + 1;
- 遍历 t,给每个字母对应的索引 - 1;
- 最后检查数组是否全为 0(次数相同)。
javascript
var isAnagram = function(s, t) {
// 若长度不同,直接返回false(字符数不同不可能是异位词)
if (s.length !== t.length) {
return false;
}
// 用数组记录26个字母的出现次数(初始全为0)
let hash = new Array(26).fill(0);
// 遍历s,给对应字母的计数+1
for(let i = 0; i < s.length; i++){
// 计算字母对应的索引(a→0,b→1,...,z→25)
const index = s.charCodeAt(i) - 'a'.charCodeAt(0);
hash[index]++;
}
// 遍历t,给对应字母的计数-1
for(let i = 0; i < t.length; i++){
const index = t.charCodeAt(i) - 'a'.charCodeAt(0);
hash[index]--;
// 若计数为负数,说明t有s没有的字母,直接返回false
if(hash[index] < 0) return false;
}
// 检查数组是否全为0(所有字母次数匹配)
for(let count of hash){
if(count !== 0) return false;
}
return true;
};
为什么用数组?
- 26 个字母范围固定,数组大小确定(26),内存占用小;
- 索引访问速度比 Set/Map 更快(直接操作内存地址)。
(2)Set:适合数值范围大或分散,只需判断 "是否出现"
适用场景:
- 元素是大范围整数、字符串(比如用户 ID、随机数);
- 只需知道 "元素是否存在",不需要记录次数或其他信息。
原理:
Set 是 "值的集合",它的has()
方法能快速判断一个值是否存在(底层是哈希表实现,O (1) 时间)。比数组更灵活 ------ 数组需要提前知道范围,Set 可以动态存储任意值。
例题

解题思路:
- 数组元素可能很大(比如 10⁹),用数组浪费空间;
- 用 Set 存储 nums1 的元素,再遍历 nums2,判断元素是否在 Set 中,存在则加入结果。
javascript
function intersection(nums1, nums2) {
// 用Set存储nums1的元素,去重且支持快速查找
const set1 = new Set(nums1);
const resultSet = new Set(); // 用Set存结果,避免重复
for (const num of nums2) {
// 判断num是否在set1中,存在则加入结果
if (set1.has(num)) {
resultSet.add(num);
}
}
return Array.from(resultSet); // 转成数组返回
}
为什么用 Set?
- nums1 的元素可能很大(比如 10⁹),用数组需要开辟 10⁹大小的空间,完全不现实;
- Set 自动去重,刚好满足 "结果元素唯一" 的需求;
has()
方法比数组的includes()
快(数组includes()
是 O (n) 遍历,Set 是 O (1) 哈希查找)。
(3)Map:适合需要存储 "键值对"(如次数、下标)的场景
适用场景:
- 需要记录 "元素出现的次数""元素对应的下标" 等额外信息;
- 键和值是一一对应的关系(比如元素→次数、元素→索引)。
原理:
Map 是 "键值对的集合",键可以是任意类型,get()
/set()
方法都是 O (1) 时间复杂度。比 Set 多了 "值" 的存储,比数组更灵活(键不需要是连续整数)。
算法题实战:LeetCode 1. 两数之和
问题 :找出数组中两个数的和等于目标值,返回它们的下标。
示例 :nums = [2,7,11,15], target = 9
→ 输出[0,1]
。

解题思路:
- 遍历数组时,用 Map 存储 "已遍历的元素→下标";
- 对于当前元素
nums[i]
,计算目标值target - nums[i]
,判断是否在 Map 中; - 存在则返回两个下标,不存在则将当前元素和下标存入 Map。
javascript
function twoSum(nums, target) {
const map = new Map(); // 存储键:元素值,值:元素下标
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i]; // 需要找的另一个元素
// 判断complement是否在Map中
if (map.has(complement)) {
return [map.get(complement), i]; // 返回两个下标
}
// 不在则存入当前元素和下标
map.set(nums[i], i);
}
return []; // 题目说一定有解,可省略
}
为什么用 Map?
- 需要同时存储 "元素值" 和 "下标"(键值对关系),Set 只能存值,数组无法灵活映射;
- 一次遍历就能完成,时间复杂度 O (n),比暴力解法(O (n²))高效得多。
3 种哈希实现的核心区别:一张表讲清楚
实现方式 | 适用场景 | 优势 | 劣势 | 典型用法 |
---|---|---|---|---|
数组 | 有限范围的整数(如 0-255) | 访问速度最快,内存占用小 | 需提前知道范围,不适合字符串 / 大数值 | 计数(字母、分数等) |
Set | 任意类型,只需判断 "是否存在" | 动态存储,自动去重 | 不能存额外信息(如次数、下标) | 交集、去重、判断存在性 |
Map | 需要存储键值对(如元素→次数 / 下标) | 灵活存储任意键值,支持复杂逻辑 | 比数组 / Set 稍占内存 | 两数之和(元素→下标)、统计次数 |
面试中常考的 "坑":什么时候不能用哈希表?
哈希表虽然高效,但也有局限性,面试中可能会被问到:
- 数据范围极小 :比如判断一个数是否在 [1,5] 中,用数组
[false, true, true, true, true, true]
更简单,没必要用 Set。 - 需要有序遍历 :哈希表的存储是无序的,如果需要按插入顺序或大小顺序遍历,可能需要配合数组(比如用 Map 存键值,再用
Array.from(map.keys())
转数组排序)。 - 内存限制严格:哈希表的空间复杂度是 O (n),如果数据量极大(如 10⁸)且内存有限,可能需要用排序 + 双指针(空间 O (1))替代。
总结:记住这个选择流程,解题不纠结
遇到 "判断元素是否出现" 类问题时,按这个流程选哈希实现:
看元素类型和范围:
- 是有限范围的整数(如 a-z、0-100)→ 用数组;
- 是大范围整数 / 字符串,且只需判断 "是否存在"→ 用Set;
- 需要存储键值对(次数、下标等)→ 用Map。