面试官:如何判断元素是否出现过?我:三种哈希方法任你选

什么是哈希?什么时候必须用哈希?数组、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. 数据范围极小 :比如判断一个数是否在 [1,5] 中,用数组[false, true, true, true, true, true]更简单,没必要用 Set。
  2. 需要有序遍历 :哈希表的存储是无序的,如果需要按插入顺序或大小顺序遍历,可能需要配合数组(比如用 Map 存键值,再用Array.from(map.keys())转数组排序)。
  3. 内存限制严格:哈希表的空间复杂度是 O (n),如果数据量极大(如 10⁸)且内存有限,可能需要用排序 + 双指针(空间 O (1))替代。

总结:记住这个选择流程,解题不纠结

遇到 "判断元素是否出现" 类问题时,按这个流程选哈希实现:

看元素类型和范围

  • 是有限范围的整数(如 a-z、0-100)→ 用数组
  • 是大范围整数 / 字符串,且只需判断 "是否存在"→ 用Set
  • 需要存储键值对(次数、下标等)→ 用Map
相关推荐
工业甲酰苯胺2 小时前
TypeScript枚举类型应用:前后端状态码映射的最简方案
javascript·typescript·状态模式
brzhang2 小时前
我操,终于有人把 AI 大佬们 PUA 程序员的套路给讲明白了!
前端·后端·架构
止观止3 小时前
React虚拟DOM的进化之路
前端·react.js·前端框架·reactjs·react
goms3 小时前
前端项目集成lint-staged
前端·vue·lint-staged
谢尔登3 小时前
【React Natve】NetworkError 和 TouchableOpacity 组件
前端·react.js·前端框架
Lin Hsüeh-ch'in3 小时前
如何彻底禁用 Chrome 自动更新
前端·chrome
珊瑚里的鱼3 小时前
LeetCode 692题解 | 前K个高频单词
开发语言·c++·算法·leetcode·职场和发展·学习方法
秋说4 小时前
【PTA数据结构 | C语言版】顺序队列的3个操作
c语言·数据结构·算法
augenstern4165 小时前
HTML面试题
前端·html
张可5 小时前
一个KMP/CMP项目的组织结构和集成方式
android·前端·kotlin