文章目录
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 <= 10^4
0 <= strs[i].length <= 100
strs[i] 仅包含小写字母
思路(排序法√):
字母异位词:由相同字母、不同顺序组成的单词
比如:eat / tea / ate 是一组
tan / nat 是一组
要求:把所有互为字母异位词的单词分到同一个列表里。
用「排序后的字符串」做 key,把互为异位词的单词存在同一个 value 里
- 对每个字符串按字母排序
eat→ 排序 →aettea→ 排序 →aettan→ 排序 →ant
- 用 map 存储
- key:排序后的字符串(唯一标识一组异位词)
- value:这一组的所有单词
- 最后把 map 里的所有 value 拿出来,就是答案
代码实现(Go):
go
package main
import (
"fmt"
"sort"
)
// groupAnagrams 字母异位词分组
// 核心思想:字母异位词排序后结果相同,用排序后的字符串作为唯一 key 进行分组
func groupAnagrams(strs []string) [][]string {
// 定义哈希表(map)
// key :string 类型 → 排序后的字符串(同一组异位词的唯一标识)
// value:[]string 类型 → 存储属于当前分组的所有原始字符串
hashMap := make(map[string][]string)
// 遍历输入字符串数组中的每一个字符串
for _, str := range strs {
// 将字符串转换为 byte 切片,方便排序
// 示例:"eat" -> []byte{'e', 'a', 't'}
bytes := []byte(str)
// 对 byte 切片进行升序排序
// 排序后:[]byte{'e','a','t'} → []byte{'a','e','t'}
// 所有字母异位词排序后都会得到相同的 byte 切片
// sort.Slice 通过自定义比较函数来排序切片,这里用 sort.Slice 对 byte 排序,本质是按 ASCII 升序
sort.Slice(bytes, func(i, j int) bool {
return bytes[i] < bytes[j]
})
// 将排序后的 byte 切片转回字符串,作为当前分组的 key
// 示例:[]byte{'a','e','t'} → "aet"
key := string(bytes)
// 将当前字符串,添加到哈希表中对应 key 的分组里
// 若 key 不存在,map 会自动创建;若已存在,则追加到切片末尾
hashMap[key] = append(hashMap[key], str)
}
// 定义结果切片,用于存放最终的分组结果
result := [][]string{}
// 遍历哈希表,将所有分组依次加入结果切片
for _, group := range hashMap {
result = append(result, group)
}
// 返回最终分组结果
return result
}
func main() {
// 输入:待分组的字符串数组
inputStrs := []string{"eat", "tea", "tan", "ate", "nat", "bat"}
// 调用字母异位词分组函数,得到输出结果
output := groupAnagrams(inputStrs)
// 输出:[[eat tea ate] [tan nat] [bat]]
fmt.Println(output)
}
- 时间复杂度 :O(n * k log k)
- n:字符串个数
- k:单个字符串最大长度
- 每个字符串排序:k log k
- 空间复杂度 :O(n * k)
- 存储所有字符串
思路(计数法×):
字母异位词 ↔ 字母出现次数完全一样
比如:
eat:a(1)、e(1)、t(1)tea:a(1)、e(1)、t(1)
→ 次数一样 → 同一组
- 每个字符串用 长度 26 的数组 统计 a-z 出现次数
- 把这个计数数组变成唯一 key
- 用
map把 key 相同的单词放一起 - 最后输出所有组
代码实现(Go):
go
package main
import "fmt"
// 字母异位词的字符出现次数一定完全相同,用次数数组作为 key 分组
func groupAnagrams(strs []string) [][]string {
// map 的 key 是 [26]int 数组
// 因为 Go 中数组可以直接比较,所以【完全相同的数组】会对应【同一个 key】
// key:[26]int → 记录 a-z 每个字母出现的次数
// value:[]string → 属于这一组的所有字母异位词
mp := make(map[[26]int][]string)
// 遍历输入的每一个字符串
for _, str := range strs {
// 1. 创建一个长度 26 的数组,用来统计当前字符串每个字母出现次数
// 下标 0 → 'a',下标 1 → 'b' ... 下标 25 → 'z'
cnt := [26]int{}
// 2. 遍历当前字符串里的每一个字符
for _, ch := range str {
// ch - 'a' 把字符变成 0~25 的下标
// 例:'a'-'a' = 0,'e'-'a' = 4
// 然后对应位置计数 +1
cnt[ch-'a']++
}
// 3. 核心:把【计数数组】当作 key
// 只要两个字符串是字母异位词,cnt 数组一定一模一样 → key 相同 → 分到一组
// 将当前字符串 str,添加到 map 中 key 为 cnt 对应的字符串切片中
// 如果 cnt 不存在于 map 中,则自动创建 key=cnt,并将 str 作为切片的第一个元素
mp[cnt] = append(mp[cnt], str)
}
// 4. 构造返回结果
// len(mp) = 最终结果的长度
// 预先分配容量,效率更高,如果不写容量:Go 会自动扩容,不够就加,不够就加,频繁扩容变慢。
// 创建一个空的二维字符串切片,但预先告诉 Go:这个切片未来要装 len(mp) 个元素,提前分配好空间
res := make([][]string, 0, len(mp))
// 遍历 map,把每一组单词放进结果里
for _, group := range mp {
res = append(res, group)
}
return res
}
func main() {
// 输入:待分组的字符串数组
inputStrs := []string{"eat", "tea", "tan", "ate", "nat", "bat"}
// 调用字母异位词分组函数,得到输出结果
output := groupAnagrams(inputStrs)
// 输出:[[eat tea ate] [tan nat] [bat]]
fmt.Println(output)
}
时间复杂度:O(N × K)
- N:字符串个数
- K:字符串最大长度
- 每个字符只遍历一次,无排序,比排序法更快
空间复杂度:O(N × K)
- 存储所有字符串
- 计数数组固定大小 26,是常数空间
计数法虽然时间复杂度更优(O(n*k)),但它依赖固定字符集(如 a-z),通用性较差;而排序法适用于任意字符