前言
我们来到了第二道题目,第二道题目为什么不是第二篇呢。因为计算机专业都是从0开始计位😆
题目 49.字母异位词分组 【中等】
这是leetcode热题的第二道题目。和two sum一样属于哈希的题目。 不过这道题目也有多张不同的解法,难度中等。 对于中等难度的题目,大部分都是有简单的时间的思路。我们在有多种解法的时候可以选择最容易实现的解法。
题目描述
给你一个字符串数组 strs
,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例 1:
css
输入:strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出:[["eat","tea","ate"],["tan","nat"],["bat"]]
示例 2:
lua
输入:strs = [""]
输出:[[""]]
示例 3:
lua
输入:strs = ["a"]
输出:[["a"]]
提示:
1 <= strs.length <= 10⁴
0 <= strs[i].length <= 100
strs[i]
仅包含小写字母
题目分析
看到这个题目首先觉得,其实用眼睛看就可以看出答案了。人脑对小数据集的计算和视觉分析是非常厉害,人类的进化出来的天赋吧。 不过我们毕竟是做题目所以要写出计算机能够理解的解题步骤。
排序法
-
理解题目 什么是字母异位词,简单来说,就是两个或多个词语虽然它们的字母顺序不同,但包含的字母以及每个字母的数量是完全一样的。换句话说,如果把一个词中的字母重新排列组合能得到另一个词,那么这两个词就是字母异位词。
-
题目给出一个单词数组,我们要做的是找出其中是字母异位的词,把他们进行分组。 比如:["eat", "tea", "tan", "ate", "nat", "bat"]。 其中肉眼观察eat,eat,ate是字母异位词。tan和nat一组。bat自己一组。
-
先试着人脑自然思考分组的时候,其实我们做了一个什么操作。把eat按照某种顺序重新排序和后面的单词进行比较。eat重新排序后可以表示位tea,和后面的tea完全一样。符合条件可以分为一组。
-
但是某种顺序到底用什么顺序好呢?我们要统一用一种顺序,写起来方便。要不然一会用这个,一会用那个乱套了。一般就使用字母的自然顺序,也就是abcdefg字母从小到大的顺序,常见,并且各种语言里面都是默认按照自然顺序提供的sort方法。
-
这样eat->aet, tea->aet 排序转化后比较完全相等。他们就是字母异位词。
-
在这个过程里面我们如何快速找到排序后相同的字母异位词呢?暴力做法是遍历整个数组一次,这样很容易超时。上一篇有讲到,用哈希结构。哈希结构帮助我们提前分组,可以快速定位到一个元素是否存在。
-
哈希结构就是Map<String, List>。例如: aet作为key,value是原始的词的数组。这样就把互为字母异位词的数据分到了一个List里面。 示例结构:
json
{
"aet": ["eat", "tea", "ate"],
"ant": ["tan", "nat"],
"abt": ["bat"]
}
总结:eat,aet,tea本质上是相同的字母顺序乱了。我们总过排序他们归到固定的字母从小到大的顺序后,结果相同的就是字母异位词。在处理的过程中使用哈希表来存储按照从小到大重排序的单词,可以快速找到相同的单词。
字母计数法
字母计数法本质上是桶排,或者说哈希思想的一种应用。
想象一个游戏:颜色小球分类游戏
-
准备26个桶
就像你有26个颜色的桶,对应英文字母的26个字母(a到z)。每个桶只装一种颜色的小球,比如a是红色,b是橙色,c是黄色......依此类推。
-
装小球
你现在有一个单词,比如
"eat"
,你就把一个红色小球(e)、一个蓝色小球(a)、一个绿色小球(t)分别丢进相应的桶里。 -
再来一个单词
比如再来一个单词
"tea"
,你也丢一个t、一个e、一个a进去------丢到的桶跟上一个单词一样,丢进去的颜色和数量也一样。 -
比较桶里的小球
我们不看单词长啥样,只看桶里的小球颜色和数量是不是一模一样。如果一样,那就说明这两个单词其实就是用同样的字母组成的,只是顺序不一样------它们就是字母异位词
ps:字母计数法的思想在做字符串统计的相关题目的时候经常会使用。 统计字符串里面字符出现的次数,对应都有哪些字符。
Java
int[] dict = new int[26];
for(char c: str){
dict[c - 'a']++; // char本质上是int类型数据,映射'a'为0位置。
}
两种方法时间和空间复杂度比较
方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
---|---|---|---|
排序法 | O(n * k log k) |
O(n * k) |
所有字符集合 |
字母计数法 | O(n * k) |
O(n) |
固定字母集(如a-z) |
排序的时间复杂度
- 假设每个单词长度是
k
,总共有n
个单词。 - 每个单词要排序,排序复杂度是
O(k log k)
。 - 所以总时间是:
O(n * k log k)
排序的空间复杂度:
- 排序时需要额外的空间存排序结果,平均是
O(k)
,总共是O(n * k)
。
字母计数法时间复杂度:
- 每个单词扫描一次,构建频率数组是
O(k)
- 总共是:
O(n * k)
- 数组转换成字符串(作为哈希 key)是常数时间,因为长度是固定的 26。
字母计数法空间复杂度:
- 每个单词对应一个长度为 26 的数组,所以空间是
O(n * 26)
,也就是O(n)
级别。
ps:虽然理论上来说字母计数法的时间复杂度要小于排序,不过因为leetcode上测试单词都很短,所以我跑出来的结果是排序的时间要小于字母计数法的时间。
java 排序实现
arduino
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String str : strs) {
char[] arr = str.toCharArray();
Arrays.sort(arr); // 排序归一
String key = new String(arr);
map.computeIfAbsent(key, k -> new ArrayList<>()).add(str); // 如果存在就加入到list里面,不存在就创建新的list加入进去
}
return new ArrayList<>(map.values());
}
}
c++ 排序实现
arduino
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> map;
for (string& str : strs) {
string key = str;
sort(key.begin(), key.end()); // 排序作为key
map[key].push_back(str);//c++ 不存在会默认初始化。
}
vector<vector<string>> result;
for (auto& pair : map) {
result.push_back(pair.second);
}
return result;
}
};
java 字母计数法
arduino
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];
for (char c : str.toCharArray()) {
count[c - 'a']++;
}
// 构建唯一字符串key(例如:#1#0#2#0...)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 26; i++) {
sb.append('#'); // 避免 "11" 与 "1|1" 混淆
sb.append(count[i]);
}
String key = sb.toString();
map.computeIfAbsent(key, k -> new ArrayList<>()).add(str);
}
return new ArrayList<>(map.values());
}
}
C++ 字母计数法
arduino
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> map;
for (string& str : strs) {
int count[26] = {0};
for (char c : str) {
count[c - 'a']++;
}
// 构建 key:#1#0#2#0...
string key;
for (int i = 0; i < 26; ++i) {
key += "#" + to_string(count[i]);
}
map[key].push_back(str);
}
vector<vector<string>> result;
for (auto& pair : map) {
result.push_back(pair.second);
}
return result;
}
};
总结
排序,桶排,哈希的思想在很多题目都会常见。通过做100道具有代表的题目,总结解题的方法,来解决没有做过的类似题目。 下一道题见 🚀