哈希(题单顺序)
热题 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]
- i=0,x=2,need=7,map 里没有 7 → 存
- 重复值是否会出问题?:不会。题目保证有解,我们只要找到任意一对即可;
Map存某个值的一个下标就足够。
复杂度
- 时间:(O(n))
- 空间:(O(n))
类似题目(“补数”套路)
// 目标:找一对/一组元素满足某种和/差条件const map = new Map() // value -> index / countfor (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. 字母异位词分组
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
示例 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 <= 1040 <= strs[i].length <= 100strs[i]仅包含小写字母
最优解(计数签名当 key:比排序更稳)
正在加载编辑器...
最优解讲解(通俗版)
- 先搞清楚“异位词”相等的本质:顺序可以不同,但每个字母出现次数必须完全一样。例如
eat与tea:a/e/t 都各出现 1 次。 - 朴素思路的问题:如果你把每个新字符串拿去和已有分组逐个比对,最坏会变成 (O(n^2)) 的比较,非常慢。
- 要想快,就要给每个字符串做一个“身份证(key)”:
- 是异位词 → key 一定相同
- 不是异位词 → key 尽量不同(避免碰撞)
- 两种常见 key:
- 排序 key:
s.split('').sort().join('')。好理解,但每次排序成本 (O(k\log k))。 - 计数 key(本题推荐):只处理 26 个小写字母,做一个长度 26 的计数数组
cnt,扫一遍字符串就行((O(k)))。再把cnt序列化成字符串当 key。
- 排序 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. 最长连续序列
给定一个未排序的整数数组 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 = 0for (const x of set) {if (set.has(x - 1)) continue // 不是起点就跳过let y = xwhile (set.has(y + 1)) y++best = Math.max(best, y - x + 1)}