50-字母异位词分组
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
示例 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 <= 104
0 <= strs[i].length <= 100
strs[i]
仅包含小写字母
字母异位词分组的三种方法
要将字母异位词组合在一起,可以通过统计字符频率或排序字符的方式来识别它们。以下是三种不同的实现方法:
方法一:排序键哈希表法
将每个字符串排序后作为键,原字符串作为值存入哈希表,最后提取哈希表的值作为结果。
function groupAnagrams(strs: string[]): string[][] {const map = new Map<string, string[]>();for (const str of strs) {// 将字符串排序后作为键const sortedKey = str.split('').sort().join('');// 如果键不存在,初始化一个数组if (!map.has(sortedKey)) {map.set(sortedKey, []);}// 将原字符串添加到对应键的数组中map.get(sortedKey)!.push(str);}// 返回哈希表中的所有值return Array.from(map.values());
}
方法二:字符频率哈希表法
统计每个字符串中字符的频率,将频率数组转换为字符串作为键,存入哈希表。
function groupAnagrams(strs: string[]): string[][] {const map = new Map<string, string[]>();for (const str of strs) {// 统计字符频率的数组(26个小写字母)const count = new Array(26).fill(0);// 统计每个字符的出现次数for (const char of str) {count[char.charCodeAt(0) - 'a'.charCodeAt(0)]++;}// 将频率数组转换为字符串作为键const key = count.join('#');// 如果键不存在,初始化一个数组if (!map.has(key)) {map.set(key, []);}// 将原字符串添加到对应键的数组中map.get(key)!.push(str);}// 返回哈希表中的所有值return Array.from(map.values());
}
方法三:质数乘积法(理论优化,但实际可能溢出)
将每个字符映射为一个质数,通过计算字符串中所有字符对应的质数乘积作为键,确保字母异位词的乘积相同。
function groupAnagrams(strs: string[]): string[][] {// 26个小写字母对应的质数const primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29,31, 37, 41, 43, 47, 53, 59, 61, 67,71, 73, 79, 83, 89, 97, 101];const map = new Map<number, string[]>();for (const str of strs) {// 计算质数乘积作为键let product = 1;for (const char of str) {const index = char.charCodeAt(0) - 'a'.charCodeAt(0);product *= primes[index];}// 如果键不存在,初始化一个数组if (!map.has(product)) {map.set(product, []);}// 将原字符串添加到对应键的数组中map.get(product)!.push(str);}// 返回哈希表中的所有值return Array.from(map.values());
}
复杂度分析
方法 | 时间复杂度 | 空间复杂度 | 说明 |
---|---|---|---|
方法一(排序键) | O(n * k log k) | O(n * k) | n 是字符串数量,k 是最大字符串长度,排序操作主导时间复杂度 |
方法二(频率统计) | O(n * k) | O(n * k) | 直接统计字符频率,时间复杂度更优,适用于字符集较小的情况 |
方法三(质数乘积) | O(n * k) | O(n * k) | 理论上时间复杂度最优,但可能存在整数溢出问题,仅适用于字符串较短的情况 |
测试示例
// 示例1测试
console.log(groupAnagrams(["eat", "tea", "tan", "ate", "nat", "bat"]));
// 输出: [["bat"],["nat","tan"],["ate","eat","tea"]]// 示例2测试
console.log(groupAnagrams([""]));
// 输出: [[""]]// 示例3测试
console.log(groupAnagrams(["a"]));
// 输出: [["a"]]// 自定义测试
console.log(groupAnagrams(["listen", "silent", "enlist"]));
// 输出: [["listen","silent","enlist"]]
方法选择建议
- 方法一实现简单,适用于各种字符集,但时间复杂度较高,适合字符串长度较小的情况。
- 方法二是最优解,时间复杂度最低,尤其适合字符集较小的场景(如本题中的小写字母)。
- 方法三理论上时间复杂度最优,但存在整数溢出风险,实际应用中需谨慎使用。
在实际应用中,推荐使用方法二作为首选解决方案,它在效率和实现复杂度之间取得了良好平衡。
方法一的时间复杂度和空间复杂度是多少?
除了排序键哈希表法,还有其他方法可以实现字母异位词分组吗?
方法二中的计数数组法是如何工作的?