🔄 分组变位词(Group Anagrams):Java 实现与算法解析
🎯 问题描述
给定一个字符串数组 strs
,将所有互为变位词(Anagrams)的字符串分组。变位词定义为:字符相同但排列不同的字符串(如 "listen" 和 "silent")。返回分组后的列表,顺序不限。
示例输入输出
输入 |
输出 |
["eat","tea","tan","ate","nat","bat"] |
[["bat"],["nat","tan"],["ate","eat","tea"]] |
[] |
[] |
["a"] |
[["a"]] |
🧠 解决方案
方法一:排序分组法(核心解法)
核心思想
变位词排序后字符串相同,利用哈希表存储 排序后字符串 → 原字符串列表
的映射关系。
实现步骤
- 遍历数组:处理每个字符串。
- 字符串排序:转换为字符数组,排序后转回字符串(作为分组键)。
- 哈希表分组:键不存在时创建新列表,存在则添加原字符串。
- 结果提取:返回哈希表所有值(分组列表)。
代码实现(Java)
import java.util.*;
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String str : strs) {
// 生成排序后的键
char[] chars = str.toCharArray();
Arrays.sort(chars);
String key = new String(chars);
// 分组逻辑(Java 8+ 特性)
map.computeIfAbsent(key, k -> new ArrayList<>()).add(str);
}
return new ArrayList<>(map.values()); // 提取所有分组
}
}
复杂度分析
维度 |
时间复杂度 |
空间复杂度 |
最坏情况 |
O(n·m log m) |
O(n·m) |
说明 |
n = 字符串数量,m = 平均长度(排序时间) |
存储所有字符串(键值对) |
优缺点
优点 |
缺点 |
逻辑清晰,易于实现 |
排序操作有固定时间成本 |
适用于所有编程语言(通用思路) |
长字符串排序可能影响性能 |
方法二:频率统计法(进阶优化)
核心思想
统计字符频率(如 26 个字母的计数),生成唯一键(如 [1,0,0,...]
形式的字符串)。
代码实现(Java)
import java.util.*;
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String str : strs) {
int[] count = new int[26]; // 统计 a-z 频率
for (char c : str.toCharArray()) {
count[c - 'a']++;
}
// 生成唯一键(数组转字符串)
String key = Arrays.toString(count);
map.computeIfAbsent(key, k -> new ArrayList<>()).add(str);
}
return new ArrayList<>(map.values());
}
}
复杂度分析
维度 |
时间复杂度 |
空间复杂度 |
最坏情况 |
O(n·m) |
O(n·m) |
说明 |
遍历字符统计频率(无排序) |
存储频率数组和字符串 |
优缺点
优点 |
缺点 |
避免排序,适合长字符串 |
键生成依赖字符集大小(如仅支持 a-z) |
时间复杂度更优(O (m) vs O (m log m)) |
移植性稍差(需处理字符集扩展) |
📊 方法对比
维度 |
排序分组法 |
频率统计法 |
核心操作 |
排序(O (m log m)) |
频率统计(O (m)) |
键类型 |
排序后的字符串 |
频率数组字符串 |
适用场景 |
通用场景,短字符串 |
长字符串,追求极致性能 |
扩展性 |
支持任意字符集 |
需调整字符集范围(如 Unicode) |
Java 实现 |
简洁(Arrays.sort ) |
需处理数组转字符串 |
🧪 测试用例
输入 |
预期输出 |
验证点 |
["eat","tea","tan","ate","nat","bat"] |
[["bat"],["nat","tan"],["ate","eat","tea"]] |
多组变位词 |
[] |
[] |
空输入处理 |
["a"] |
[["a"]] |
单元素处理 |
["abc","cba","bac","aaa"] |
[["aaa"],["abc","cba","bac"]] |
混合字符集 |
🌟 总结与最佳实践
选择建议
场景 |
推荐方法 |
理由 |
通用场景(教学 / 竞赛) |
排序分组法 |
逻辑清晰,易于调试 |
长字符串 / 性能敏感 |
频率统计法 |
避免排序开销 |
多语言协作 |
排序分组法 |
统一逻辑,跨语言一致性 |
代码优化点
- Java 特性利用:
computeIfAbsent
简化哈希表操作。
- 内存管理:复用字符数组(避免重复创建)。
- 边界处理:空输入、单元素输入的鲁棒性验证。
关键点总结
- 变位词本质:字符相同 → 排序或频率统计生成唯一键。
- 哈希表核心作用:O (1) 时间复杂度的分组查找。
- 复杂度平衡:排序的 O (m log m) vs 频率统计的 O (m)(根据场景选择)。
📌 完整代码(含测试)
import java.util.*;
class Solution {
// 方法一:排序分组法
public List<List<String>> groupAnagramsBySort(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String str : strs) {
char[] chars = str.toCharArray();
Arrays.sort(chars);
String key = new String(chars);
map.computeIfAbsent(key, k -> new ArrayList<>()).add(str);
}
return new ArrayList<>(map.values());
}
// 方法二:频率统计法
public List<List<String>> groupAnagramsByFrequency(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String str : strs) {
int[] count = new int[26];
for (char c : str.toCharArray()) count[c - 'a']++;
String key = Arrays.toString(count);
map.computeIfAbsent(key, k -> new ArrayList<>()).add(str);
}
return new ArrayList<>(map.values());
}
// 测试用例
public static void main(String[] args) {
Solution solution = new Solution();
String[][] testCases = {
{ "eat", "tea", "tan", "ate", "nat", "bat" },
{ },
{ "a" },
{ "abc", "cba", "bac", "aaa" }
};
for (String[] strs : testCases) {
System.out.println("=== 输入: " + Arrays.toString(strs));
System.out.println("排序法输出: " + solution.groupAnagramsBySort(strs));
System.out.println("频率法输出: " + solution.groupAnagramsByFrequency(strs));
System.out.println();
}
}
}
测试输出:
=== 输入: [eat, tea, tan, ate, nat, bat]
排序法输出: [[bat], [nat, tan], [ate, eat, tea]]
频率法输出: [[bat], [nat, tan], [ate, eat, tea]]
=== 输入: []
排序法输出: []
频率法输出: []
=== 输入: [a]
排序法输出: [[a]]
频率法输出: [[a]]
=== 输入: [abc, cba, bac, aaa]
排序法输出: [[aaa], [abc, cba, bac]]
频率法输出: [[aaa], [abc, cba, bac]]
📖 参考与延伸
- LeetCode 题目:49. Group Anagrams
- 字符编码扩展:处理 Unicode 字符时,改用哈希表统计字符频率(替代固定长度数组)。
- 性能优化:使用 Apache Commons 的
StringUtils
或自定义排序算法(如基数排序)。