哈希表与 Map / Set 实战

本节目标

哈希表原理:把"键"通过哈希函数映射成数组下标,理想情况 O(1) 读写。冲突(不同键映射到同一下标)用"链地址法"(同桶挂链表)或"开放寻址"解决。

// 运行环境:Node.js 14+
// 保存为 dsa-l8.js,执行:node dsa-l8.js

// 两数之和:暴力 O(n²) vs 哈希 O(n)
function twoSum(nums, target) {
  const seen = new Map()
  for (let i = 0; i < nums.length; i++) {
    const need = target - nums[i]
    if (seen.has(need)) return [seen.get(need), i]
    seen.set(nums[i], i)
  }
  return []
}

// 第一个不重复字符
function firstUniqChar(s) {
  const cnt = new Map()
  for (const ch of s) cnt.set(ch, (cnt.get(ch) || 0) + 1)
  for (let i = 0; i < s.length; i++) if (cnt.get(s[i]) === 1) return i
  return -1
}

// 字母异位词分组
function groupAnagrams(words) {
  const m = new Map()
  for (const w of words) {
    const key = w.split('').sort().join('') // 排序后作指纹
    if (!m.has(key)) m.set(key, [])
    m.get(key).push(w)
  }
  return [...m.values()]
}

console.log(twoSum([2, 7, 11, 15], 9))      // [0, 1]
console.log(firstUniqChar('leetcode'))      // 0
console.log(groupAnagrams(['eat', 'tea', 'tan', 'ate', 'nat'])) // [[eat,tea,ate],[tan,nat]]

Set 去重与交集:new Set(arr) 一键去重;[...new Set(a)].filter(x=>setB.has(x)) 求交集。

名词解释

课后练习

  1. 两数之和用哈希为什么是 O(n)?
    • 答案:只遍历一次,每次 Map.has/get/set 平均 O(1);用"已见数字"换"不必回看",把暴力双重循环压成单循环。
  2. 字母异位词分组为什么用"排序后的词"当 key?
    • 答案:异位词字母组成相同,排序后必然得到同一个字符串,天然可作分组指纹;Map 按指纹聚合即得各组。

总结

哈希表是工程里使用频率最高的数据结构,没有之一。它的魔力在于把"大海捞针"的 O(n) 查找压成平均 O(1)——代价只是多花一份内存存索引。理解它,首先要接受"哈希冲突不可避免",所以才有链地址法、开放寻址这些兜底方案;其次要明白 JS 的 Map/Set 已经把这套机制封装得很好,日常几乎不用手写哈希函数。我常提醒:能用 Map/Set 把 O(n²) 降到 O(n) 的地方,就不要忍受暴力。但也别迷信 O(1)——极端哈希冲突或超大负载因子会让它退化;而且哈希表不保序、占内存。把"键→值映射 + 快速查找"这个需求刻进脑子,你会在无数场景(缓存、计数、去重、索引)里自然掏出它。