当前:LC49 · 字母分拣工厂 · 异位词分组 · 首次出现于 Day 3 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC49数据结构哈希分组 · key 身份证设计哈希记忆表

字母分拣工厂 · 异位词分组

把 ["eat","tea","tan","ate","nat","bat"] 中互为字母异位词的字符串分到同一组。

题目是什么

把 ["eat","tea","tan","ate","nat","bat"] 中互为字母异位词的字符串分到同一组。

解决什么问题

哈希表中保存的是过去的元素、频次,还是它们的位置?

核心结论

传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。

01交互算法精讲

先说结论:这道题到底解决什么

怎样从“把 ["eat","tea","tan","ate","nat","bat"] 中互为字母异位词的字符串分到同一组。”推导出 哈希分组 · key 身份证设计,并证明每次状态变化都不会漏掉答案?

中心结论:传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。

读完必须能回答
  1. 1.暴力方案在哪里重复计算,为什么仍然是正确基线?
  2. 2.哈希表中保存的是过去的元素、频次,还是它们的位置?
  3. 3.不变量“两个单词同组 ⇔ 它们生成的 key 相同。”为什么能保证算法安全前进?
02交互算法精讲

完整题目与题意拆解

给出一个字符串数组,要求对字符串数组里面有 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}。
动画 1 · 题意扫描

先看清算法到底要维护什么

先建立输入、目标、输出和第一批状态,不急着进入模板。

Step 1/20%
题目与输入建立输入、目标与算法心智

对每个词排序后得到分组 key

正在加载算法场景...
03交互算法精讲

第一层方案:暴力做法

逐对枚举能够得到答案,但同一个查找会发生许多次;哈希表把已经见过的信息保存成一次查询。

暴力方案的价值是确认题意并提供正确性基线。它通常会覆盖所有候选, 但没有保存已经确认的信息,因此同一状态会被重新计算。

动画 2 · 暴力重复

重复工作究竟发生在哪里

把重复读取或重复搜索的区域明确标出,再决定优化必须保存什么。

Step 1/20%
先做对:建立暴力基线枚举所有候选并完整验证

对每个词排序后得到分组 key

正在加载算法场景...
优化方向:传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。
04交互算法精讲

整体地图:先做什么,再做什么

  1. 1建模把输入翻译成“哈希记忆表”,明确答案需要观察什么。
  2. 2状态只维护 当前词、生成 key、map( key → 原词列表 )、活跃仓库桶。
  3. 3转移每一步按照 传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。
  4. 4收尾读取 得到 3 组:{eat,tea,ate}、{tan,nat}、{bat}。,并复核边界与复杂度。
05交互算法精讲

哈希记忆表:核心概念

先问未来需要查询什么,再决定 map 的 key 与 value。

这道题不是为了记住一个组件名称,而是为了让状态具有可解释的语义:当前词、生成 key、map( key → 原词列表 )、活跃仓库桶。

核心不变量
  • 两个单词同组 ⇔ 它们生成的 key 相同。
动画 3 · 核心概念

建立“哈希记忆表”心智模型

用主例建立核心状态,先预测下一步,再公开正确分支和理由。

Step 1/30%
题目与输入建立输入、目标与算法心智

对每个词排序后得到分组 key

正在加载算法场景...
06交互算法精讲

核心机制:状态如何一步步变化

这道题可以将每个字符串都排序,排序完成以后,相同 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 仓库桶。每一次更新都必须保持核心不变量,而不是只让样例碰巧得到正确答案。

动画 4 · 机制构建

一次状态转移为什么成立

集中观察一次选择、计算和状态写回,让变量变化与原因同时出现。

Step 1/50%
'eat' 的 key = 'aet'key = 排序后的字符

把 'eat' 的字符排序,得到分组 key 'aet'

正在加载算法场景...
07交互算法精讲

正确性证明:为什么不会漏答案

初始化

初始化:算法开始时,全部合法候选仍在状态表示范围内;两个单词同组 ⇔ 它们生成的 key 相同。

保持

保持:执行“传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。”时,只删除已经能证明不可能的候选,并把新信息写回 当前词、生成 key、map( key → 原词列表 )、活跃仓库桶。

终止

终止:没有待处理状态或达到命中条件时,当前可观察结果就是“得到 3 组:{eat,tea,ate}、{tan,nat}、{bat}。”。

正确性抓手不是“样例跑通”,而是每一帧结束后仍能复述:两个单词同组 ⇔ 它们生成的 key 相同。
08交互算法精讲

完整执行过程

  1. 1题目与输入把 ["eat","tea","tan","ate","nat","bat"] 中互为字母异位词的字符串分到同一组。 因为:传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。
  2. 2'eat' 的 key = 'aet'异位词排序后一定相等,所以排序结果就是天然的分组键。 因为:key 设计是哈希分组的核心:让「同组」等价于「key 相等」。
  3. 3'tea' 的 key = 'aet'异位词排序后一定相等,所以排序结果就是天然的分组键。 因为:key 设计是哈希分组的核心:让「同组」等价于「key 相等」。
  4. 4'tan' 的 key = 'ant'异位词排序后一定相等,所以排序结果就是天然的分组键。 因为:key 设计是哈希分组的核心:让「同组」等价于「key 相等」。
  5. 5'ate' 的 key = 'aet'异位词排序后一定相等,所以排序结果就是天然的分组键。 因为:key 设计是哈希分组的核心:让「同组」等价于「key 相等」。
  6. 6'nat' 的 key = 'ant'异位词排序后一定相等,所以排序结果就是天然的分组键。 因为:key 设计是哈希分组的核心:让「同组」等价于「key 相等」。
  7. 7'bat' 归入 key 'abt'按 key 把词追加到对应组。 因为:map 的 value 是列表,实现「一个 key 对多个值」。
  8. 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 仓库桶。
动画 5 · 完整执行

从输入完整走到可观察结果

从主例第一帧运行到答案,时间轴始终显示当前状态、因果解释和下一步。

Step 1/80%
题目与输入建立输入、目标与算法心智

对每个词排序后得到分组 key

正在加载算法场景...
09交互算法精讲

把动画和 Go 代码逐行对应

代码窗口不会按容易漂移的固定数字行号硬绑动画,而是根据当前语义阶段, 在完整 Go 实现中定位选择、计算、写回或返回分支。当前变量与高亮行一起变化。

动画 6 · 代码映射

让每个动作都落到 Go 分支

重新执行关键分支,只显示当前代码附近窗口,并说明这行为什么在此刻运行。

Step 1/60%
建立空哈希结构准备 map / set / 计数器

对每个词排序后得到分组 key

正在加载算法场景...
10交互算法精讲

完整 Go 提交代码与最小测试

完整 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 许可代码整理,并按本站教学结构补充解释与动画映射。

11交互算法精讲

正确性与复杂度

时间复杂度 O(n·k log k) 排序 key · O(n·k) 计数 key

执行过程中只保留仍可能影响答案的状态。传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。

空间复杂度 O(n·k)

额外状态主要用于维护:当前词、生成 key、map( key → 原词列表 )、活跃仓库桶。

终局不变量
  • 两个单词同组 ⇔ 它们生成的 key 相同。
12交互算法精讲

最容易写错的地方

错误 1

直接用原字符串当 key 会把 eat 和 tea 拆开。

错误 2

只看出现过哪些字母会把 ab 和 abb 混淆。

边界复查

必须额外检查空输入、单元素、未命中或不可达情况,以及恰好落在边界的输入。

13交互算法精讲

最后复盘:带走逻辑链

  1. 1题意把 ["eat","tea","tan","ate","nat","bat"] 中互为字母异位词的字符串分到同一组。
  2. 2重复逐对枚举能够得到答案,但同一个查找会发生许多次;哈希表把已经见过的信息保存成一次查询。
  3. 3优化传送带单词经标准化机器生成顺序无关 key(排序或 26 位计数),按 key 把原词追加进 HashMap 仓库桶。
  4. 4证明两个单词同组 ⇔ 它们生成的 key 相同。
  5. 5复杂度时间 O(n·k log k) 排序 key · O(n·k) 计数 key,空间 O(n·k)
面试表达:用 HashMap 分组;关键是规范化 key——排序后 eat/tea/ate 都是 aet,原词 append 到 map[key];字符集固定时可用 26 位计数优化到 O(n·k)。
迁移练习
  • LC242 有效字母异位词
  • LC128 最长连续序列