langShiftlangShift

哈希(题单顺序)

热题 100 - 哈希分组(按题单顺序):1 / 49 / 128

本页包含题单「哈希」分组的全部题目,顺序与 list.json 保持一致。原题正文来自力扣官方页面动态拉取。

1. 两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。

你可以按任意顺序返回答案。

 

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]

 

提示:

  • 2 <= nums.length <= 104
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109
  • 只会存在一个有效答案

 

进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗?

最优解(哈希表:一边遍历一边查)
正在加载编辑器...
最优解讲解(通俗版)
  • 先从最笨的方法想:两层循环枚举所有 ((i,j)),看 nums[i] + nums[j] 是否等于 target。一定能找到,但时间是 (O(n^2)),数组一大就吃不消。
  • 优化的关键在于:别重复“找补数”。当你拿到当前数字 (x),你真正想问的是:有没有一个数等于 (target-x)?如果每次都扫一遍数组去找,复杂度就降不下来。
  • 把问题改写成“查字典”:我们一边遍历,一边用 Map 记住“某个值出现过,位置是多少”(value -> index)。
    • 走到位置 i 时,先算 need = target - nums[i]
    • 先查map.get(need) 是否存在?存在就说明之前见过这个补数,直接返回 [j, i]
    • 再存:如果没找到,就把当前 nums[i] 存进去,给后面的元素当“补数”用。
  • 为什么要“先查再存”?:避免把同一个元素用两次。比如 target=6、当前 x=3,如果先存再查,可能会把自己当成补数拿出来。
  • 用一个例子走一遍推理过程[2,7,11,15]target=9
    • i=0,x=2,need=7,map 里没有 7 → 存 2 -> 0
    • i=1,x=7,need=2,map 里有 2 -> 0 → 返回 [0,1]
  • 重复值是否会出问题?:不会。题目保证有解,我们只要找到任意一对即可;Map 存某个值的一个下标就足够。

复杂度

  • 时间:(O(n))
  • 空间:(O(n))
类似题目(“补数”套路)
// 目标:找一对/一组元素满足某种和/差条件
const map = new Map() // value -> index / count
for (let i = 0; i < arr.length; i++) {
const need = target - arr[i] // 或其它“补数”定义
if (map.has(need)) {
// 命中:用 map.get(need) 和 i 组成答案
}
map.set(arr[i], i) // 或 map.set(arr[i], (map.get(arr[i]) ?? 0) + 1)
}

49. 字母异位词分组

力扣原题:49. 字母异位词分组
Medium
数组
哈希表
字符串
排序

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

 

示例 1:

输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]

输出: [["bat"],["nat","tan"],["ate","eat","tea"]]

解释:

  • 在 strs 中没有字符串可以通过重新排列来形成 "bat"
  • 字符串 "nat""tan" 是字母异位词,因为它们可以重新排列以形成彼此。
  • 字符串 "ate" ,"eat" 和 "tea" 是字母异位词,因为它们可以重新排列以形成彼此。

示例 2:

输入: strs = [""]

输出: [[""]]

示例 3:

输入: strs = ["a"]

输出: [["a"]]

 

提示:

  • 1 <= strs.length <= 104
  • 0 <= strs[i].length <= 100
  • strs[i] 仅包含小写字母
最优解(计数签名当 key:比排序更稳)
正在加载编辑器...
最优解讲解(通俗版)
  • 先搞清楚“异位词”相等的本质:顺序可以不同,但每个字母出现次数必须完全一样。例如 eattea:a/e/t 都各出现 1 次。
  • 朴素思路的问题:如果你把每个新字符串拿去和已有分组逐个比对,最坏会变成 (O(n^2)) 的比较,非常慢。
  • 要想快,就要给每个字符串做一个“身份证(key)”
    • 是异位词 → key 一定相同
    • 不是异位词 → key 尽量不同(避免碰撞)
  • 两种常见 key
    • 排序 keys.split('').sort().join('')。好理解,但每次排序成本 (O(k\log k))。
    • 计数 key(本题推荐):只处理 26 个小写字母,做一个长度 26 的计数数组 cnt,扫一遍字符串就行((O(k)))。再把 cnt 序列化成字符串当 key。
  • 为什么 key 要用 join("#"):为了避免拼接歧义。没有分隔符时,[1,11][11,1] 这类情况可能会撞在同一个字符串上;用 # 分隔就能区分开。
  • 最后一步:按 key 分桶Map<key, string[]>,同 key 的字符串直接 push 到同一个数组里,最终输出所有桶即可。
  • 例子快走一遍["eat","tea","tan","ate","nat","bat"]
    • eat/tea/ate 的计数 key 相同 → 一桶
    • tan/nat 一桶
    • bat 一桶
  • 易错点
    • 如果题目字符集不是 26 个小写字母(比如包含大写/Unicode),计数 key 需要相应调整;否则建议用“排序 key”更通用。
    • 计数数组序列化时最好加分隔符(如 join("#")),避免拼接歧义。

复杂度

设总字符数为 (S):

  • 时间:(O(S))(只遍历字符;生成 key 视作常数 26)
  • 空间:(O(S))
类似题目(“签名分桶”套路)
const buckets = new Map()
for (const item of items) {
const key = buildSignature(item) // 排序签名 / 计数签名 / 结构化签名
;(buckets.get(key) ?? buckets.set(key, []).get(key)!).push(item)
}
return [...buckets.values()]

128. 最长连续序列

力扣原题:128. 最长连续序列
Medium
并查集
数组
哈希表

给定一个未排序的整数数组 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
最优解(哈希集合 + 只从“起点”扩展)
正在加载编辑器...
最优解讲解(通俗版)
  • 先想最直接的方法:排序后线性扫一遍统计连续段长度,确实能做,但排序要 (O(n\log n))。
  • 关键洞察:我们只需要“某个数在不在”,并不需要排序后的顺序。所以把所有数放进 Set,用均摊 (O(1)) 判断 x 是否存在。
  • 为什么不能从每个数都往右数?:会重复计数。比如 1,2,3,4,从 1 数一次、从 2 再数一次,很多工作是重复的。
  • 核心推理:只从“真正的起点”出发,就不会重复
    • 如果 x-1 在集合里,说明 x 不是起点(它属于某段从更小数开始的连续段),此时从 x 开始往右数只会重复别人已经数过的部分。
    • 只有当 x-1 不在集合里时,x 才是起点,我们才从 x 往右扩展:x, x+1, x+2...
  • 为什么整体是 (O(n))(均摊)?
    • 每个元素最多被当作“起点候选”检查一次(常数操作);
    • 也最多在 while 扩展中被走过一次(因为只有从起点那次扩展会走到它)。
    • 所以总步数是线性的。
  • 用例子走一遍[100,4,200,1,3,2]
    • set = {1,2,3,4,100,200}
    • 看 1:0 不在 set → 起点,往右数到 4 → 长度 4
    • 看 2:1 在 set → 不是起点,跳过(避免重复)
    • 看 100:99 不在 → 长度 1;看 200:199 不在 → 长度 1
    • 最大长度就是 4
  • 重复元素怎么办?Set 会自动去重,不影响连续段的定义。
  • 易错点:最好 for (const x of set) 遍历去重后的集合(而不是遍历原数组),这样能少做重复的“起点判定”。

复杂度

  • 时间:(O(n))(均摊)
  • 空间:(O(n))
类似题目(“起点扩展”套路)
const set = new Set(nums)
let best = 0
for (const x of set) {
if (set.has(x - 1)) continue // 不是起点就跳过
let y = x
while (set.has(y + 1)) y++
best = Math.max(best, y - x + 1)
}