字母分拣工厂 · 异位词分组
把 ["eat","tea","tan","ate","nat","bat"] 中互为字母异位词的字符串分到同一组。
把 ["eat","tea","tan","ate","nat","bat"] 中互为字母异位词的字符串分到同一组。
哈希表中保存的是过去的元素、频次,还是它们的位置?
传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。
先说结论:这道题到底解决什么
怎样从“把 ["eat","tea","tan","ate","nat","bat"] 中互为字母异位词的字符串分到同一组。”推导出 哈希分组 · key 身份证设计,并证明每次状态变化都不会漏掉答案?
中心结论:传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。
- 1.暴力方案在哪里重复计算,为什么仍然是正确基线?
- 2.哈希表中保存的是过去的元素、频次,还是它们的位置?
- 3.不变量“两个单词同组 ⇔ 它们生成的 key 相同。”为什么能保证算法安全前进?
完整题目与题意拆解
给出一个字符串数组,要求对字符串数组里面有 Anagrams 关系的字符串进行分组。Anagrams 关系是指两个字符串的字符完全相同,顺序不同,两者是由排列组合组成。
在本站主例中,把 ["eat","tea","tan","ate","nat","bat"] 中互为字母异位词的字符串分到同一组。
算法最终需要得到或观察:得到 3 组:{eat,tea,ate}、{tan,nat}、{bat}。
- • 输入:把 ["eat","tea","tan","ate","nat","bat"] 中互为字母异位词的字符串分到同一组。
- • 机器需要维护:当前词、生成 key、map( key → 原词列表 )、活跃仓库桶。
- • 最终可观察结果:得到 3 组:{eat,tea,ate}、{tan,nat}、{bat}。
先看清算法到底要维护什么
先建立输入、目标、输出和第一批状态,不急着进入模板。
对每个词排序后得到分组 key
第一层方案:暴力做法
逐对枚举能够得到答案,但同一个查找会发生许多次;哈希表把已经见过的信息保存成一次查询。
暴力方案的价值是确认题意并提供正确性基线。它通常会覆盖所有候选, 但没有保存已经确认的信息,因此同一状态会被重新计算。
重复工作究竟发生在哪里
把重复读取或重复搜索的区域明确标出,再决定优化必须保存什么。
对每个词排序后得到分组 key
整体地图:先做什么,再做什么
- 1建模把输入翻译成“哈希记忆表”,明确答案需要观察什么。
- 2状态只维护 当前词、生成 key、map( key → 原词列表 )、活跃仓库桶。
- 3转移每一步按照 传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。
- 4收尾读取 得到 3 组:{eat,tea,ate}、{tan,nat}、{bat}。,并复核边界与复杂度。
哈希记忆表:核心概念
先问未来需要查询什么,再决定 map 的 key 与 value。
这道题不是为了记住一个组件名称,而是为了让状态具有可解释的语义:当前词、生成 key、map( key → 原词列表 )、活跃仓库桶。
- • 两个单词同组 ⇔ 它们生成的 key 相同。
建立“哈希记忆表”心智模型
用主例建立核心状态,先预测下一步,再公开正确分支和理由。
对每个词排序后得到分组 key
核心机制:状态如何一步步变化
这道题可以将每个字符串都排序,排序完成以后,相同 Anagrams 的字符串必然排序结果一样。把排序以后的字符串当做 key 存入到 map 中。遍历数组以后,就能得到一个 map,key 是排序以后的字符串,value 对应的是这个排序字符串以后的 Anagrams 字符串集合。最后再将这些 value 对应的字符串数组输出即可。
传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。
执行过程中持续维护:当前词、生成 key、map( key → 原词列表 )、活跃仓库桶。
正确性依赖以下不变量:两个单词同组 ⇔ 它们生成的 key 相同。
面试时可以压缩为:用 HashMap 分组;关键是规范化 key——排序后 eat/tea/ate 都是 aet,原词 append 到 map[key];字符集固定时可用 26 位计数优化到 O(n·k)。
落到当前题,执行机制可以压缩为:传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。每一次更新都必须保持核心不变量,而不是只让样例碰巧得到正确答案。
一次状态转移为什么成立
集中观察一次选择、计算和状态写回,让变量变化与原因同时出现。
把 'eat' 的字符排序,得到分组 key 'aet'
正确性证明:为什么不会漏答案
初始化:算法开始时,全部合法候选仍在状态表示范围内;两个单词同组 ⇔ 它们生成的 key 相同。
保持:执行“传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。”时,只删除已经能证明不可能的候选,并把新信息写回 当前词、生成 key、map( key → 原词列表 )、活跃仓库桶。
终止:没有待处理状态或达到命中条件时,当前可观察结果就是“得到 3 组:{eat,tea,ate}、{tan,nat}、{bat}。”。
完整执行过程
- 1题目与输入把 ["eat","tea","tan","ate","nat","bat"] 中互为字母异位词的字符串分到同一组。 因为:传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。
- 2'eat' 的 key = 'aet'异位词排序后一定相等,所以排序结果就是天然的分组键。 因为:key 设计是哈希分组的核心:让「同组」等价于「key 相等」。
- 3'tea' 的 key = 'aet'异位词排序后一定相等,所以排序结果就是天然的分组键。 因为:key 设计是哈希分组的核心:让「同组」等价于「key 相等」。
- 4'tan' 的 key = 'ant'异位词排序后一定相等,所以排序结果就是天然的分组键。 因为:key 设计是哈希分组的核心:让「同组」等价于「key 相等」。
- 5'ate' 的 key = 'aet'异位词排序后一定相等,所以排序结果就是天然的分组键。 因为:key 设计是哈希分组的核心:让「同组」等价于「key 相等」。
- 6'nat' 的 key = 'ant'异位词排序后一定相等,所以排序结果就是天然的分组键。 因为:key 设计是哈希分组的核心:让「同组」等价于「key 相等」。
- 7'bat' 归入 key 'abt'按 key 把词追加到对应组。 因为:map 的 value 是列表,实现「一个 key 对多个值」。
- 8收尾与复杂度得到 3 组:{eat,tea,ate}、{tan,nat}、{bat}。 因为:时间 O(n·k log k) 排序 key · O(n·k) 计数 key · 空间 O(n·k)。传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。
从输入完整走到可观察结果
从主例第一帧运行到答案,时间轴始终显示当前状态、因果解释和下一步。
对每个词排序后得到分组 key
把动画和 Go 代码逐行对应
代码窗口不会按容易漂移的固定数字行号硬绑动画,而是根据当前语义阶段, 在完整 Go 实现中定位选择、计算、写回或返回分支。当前变量与高亮行一起变化。
让每个动作都落到 Go 分支
重新执行关键分支,只显示当前代码附近窗口,并说明这行为什么在此刻运行。
对每个词排序后得到分组 key
完整 Go 提交代码与最小测试
import "sort"
type sortRunes []rune
func (s sortRunes) Less(i, j int) bool {
return s[i] < s[j]
}
func (s sortRunes) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s sortRunes) Len() int {
return len(s)
}
func groupAnagrams(strs []string) [][]string {
record, res := map[string][]string{}, [][]string{}
for _, str := range strs {
sByte := []rune(str)
sort.Sort(sortRunes(sByte))
sstrs := record[string(sByte)]
sstrs = append(sstrs, str)
record[string(sByte)] = sstrs
}
for _, v := range record {
res = append(res, v)
}
return res
}func main() {
// 1. 主例
// 输入:mode="group-anagrams", words=["eat","tea","tan","ate","nat","bat"]
// 期望:得到 3 组:{eat,tea,ate}、{tan,nat}、{bat}。
//
// 2. 失败 / 未命中
// 检查:直接用原字符串当 key 会把 eat 和 tea 拆开。
//
// 3. 边界
// 空输入、单元素、最小合法规模,以及答案恰好落在边界的情况。
//
// 4. 迁移
// LC242 有效字母异位词;LC128 最长连续序列
}Go 参考实现基于 halfrost/LeetCode-Go 的 MIT 许可代码整理,并按本站教学结构补充解释与动画映射。
正确性与复杂度
执行过程中只保留仍可能影响答案的状态。传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。
额外状态主要用于维护:当前词、生成 key、map( key → 原词列表 )、活跃仓库桶。
- • 两个单词同组 ⇔ 它们生成的 key 相同。
最容易写错的地方
直接用原字符串当 key 会把 eat 和 tea 拆开。
只看出现过哪些字母会把 ab 和 abb 混淆。
必须额外检查空输入、单元素、未命中或不可达情况,以及恰好落在边界的输入。
最后复盘:带走逻辑链
- 1题意把 ["eat","tea","tan","ate","nat","bat"] 中互为字母异位词的字符串分到同一组。
- 2重复逐对枚举能够得到答案,但同一个查找会发生许多次;哈希表把已经见过的信息保存成一次查询。
- 3优化传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。
- 4证明两个单词同组 ⇔ 它们生成的 key 相同。
- 5复杂度时间 O(n·k log k) 排序 key · O(n·k) 计数 key,空间 O(n·k)
- • LC242 有效字母异位词
- • LC128 最长连续序列