leetcode hot 100 第二题
首先我们看看题目描述:
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例 1:
输入: strs =["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [[“bat”],[“nat”,“tan”],[“ate”,“eat”,“tea”]]示例 2:
输入: strs =[""]
输出: [[“”]]示例 3:
输入: strs =["a"]
输出: [[“a”]]提示:
- 1 <= strs.length <= 1 0 4 10^4 104
- 0 <= strs[i].length <= 100
strs[i]
仅包含小写字母
字母异位词理解: 两个字符串互为字母异位词,当且仅当两个字符串包含的字母相同。简单点说,也就是字母异位词就是包含字母相同的那些词,tap、pat和apt就互为字母异位词。
解法1 排序
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
// 定义一个哈希表 map 来存储按字母异位词分组的字符串
// 键是字符串(每个字母异位词排序后的结果),
// 值是字符串的列表(所有属于该字母异位词组的单词)。
Map<String, List<String>> map = new HashMap<String, List<String>>();
// 遍历所有输入的字符串
for(String str : strs){
// 将字符串转换为字符数组
// 这个转换的目的是为了后续排序。
char[] array = str.toCharArray();
// 对字符数组进行排序,这样字母异位词排序后会得到相同的字符串
Arrays.sort(array);
// 将排序后的字符数组转换回字符串作为哈希表的键
String key = new String(array);
// 从哈希表中取出该键对应的列表,如果没有则创建一个新的列表
List<String> list = map.getOrDefault(key, new ArrayList<String>());
// 将当前的字符串添加到列表中
list.add(str);
// 将更新后的列表放回哈希表中
map.put(key, list);
}
// 返回哈希表中所有的列表
return new ArrayList<List<String>>(map.values());
}
}
首先我们来理一理总体思路:
-
遍历输入字符串数组
对每个字符串进行处理。 -
将字符串排序
将字符串转换为字符数组,并对字符数组进行排序。排序后的结果对于互为字母异位词的字符串来说是相同的。 -
利用哈希表归类
- 以排序后的字符串作为键。
- 使用
HashMap<String, List<String>>
快速查找,判断该键是否已存在。 - 如果存在,则将原字符串加入对应的列表;如果不存在,则创建一个新的列表并添加字符串。
-
整理输出结果
将哈希表中所有的字符串列表(即各组字母异位词)转换为一个List<List<String>>
并返回。
那么观察这道题目,其实我们发现这一道题目中对于数据类型的选择非常重要。刚好借这一道题目复习java中常见的数据类型,具体可见我的另一篇博客:
结合本题「字母异位词分组」来判断数据类型和选用方法,可以从以下几个方面考虑:
-
输入数据类型
- 题目给定的是一个字符串数组(
String[] strs
)。因此,最初的数据类型是数组。数组适合存储固定数量的数据,但在题目中需要动态分组,所以接下来需要将数据分组到集合中。
- 题目给定的是一个字符串数组(
-
分组要求
- 题目要求将所有字母异位词归为一组。由于每组的元素数量不固定,因此需要使用动态数组,比如
List
。而最终返回的结果是一个包含多个字符串列表的集合,即List<List<String>>
。
- 题目要求将所有字母异位词归为一组。由于每组的元素数量不固定,因此需要使用动态数组,比如
-
如何快速查找和归类
- 当需要根据某种规则(这里是将字符串排序后的结果)来将数据归类时,哈希表(
Map
)是很好的选择。 - 具体来说,可以用
HashMap<String, List<String>>
:- 键(Key):排序后的字符串,用来表示该组的“标准形式”。
- 值(Value):一个
List<String>
,存放所有与该键对应的原字符串(即字母异位词组)。
- 使用
HashMap
可以在遍历每个字符串时,快速判断排序后的字符串是否已经作为键存在,从而决定是新建一个列表还是向已有列表中添加字符串。
- 当需要根据某种规则(这里是将字符串排序后的结果)来将数据归类时,哈希表(
-
操作数据的灵活性
ArrayList
:- 动态数组
ArrayList
实现了List
接口,可以方便地添加和遍历元素,适用于存放分组后的字符串列表。 - 最终需要将
HashMap
中的所有值转换成List<List<String>>
,因此选择ArrayList
作为返回类型。
- 动态数组
总结一下就是:
- 输入:题目提供的是
String[]
(数组)。- 分组存储:为了根据排序后的字符串快速归类,选用
HashMap<String, List<String>>
。- 分组内的数据:每组的字符串数量不定,选用
ArrayList<String>
来存储每组的字符串。- 最终返回:题目要求返回的结果是一个列表的列表,因此选用
List<List<String>>
,可以通过new ArrayList<>(map.values())
得到。
问题与解答
下面我们借助一些问题来巩固一下这道题目!
[NOTE] 问题1
问题1:Map<String, List<String>> map = new HashMap<String, List<String>>();
这句话中List<String>
是什么用法?是字符串列表吗?
- 解释:
List<String>
是 Java 泛型(Generics)的一种用法,表示一个只能存放字符串(String)的列表。泛型帮助你在编译时检查数据类型,防止放入错误类型的元素。List<String>
表示一个字符串列表,它只能存储字符串。- 常见使用场景:
- 存储数据集合:当你需要按顺序存放多个字符串时,可以使用
List<String>
。例如存储用户输入的一组单词。- 分组数据:在本题中,我们需要将所有互为字母异位词的字符串归为一组,所以每个键对应一个字符串列表。
- 示例说明:
// 定义一个只存储字符串的列表 List<String> fruits = new ArrayList<>(); fruits.add("Apple"); fruits.add("Banana"); fruits.add("Cherry"); System.out.println(fruits); // 输出:[Apple, Banana, Cherry]
[NOTE] 问题2
问题2:for(String str : strs)
这样的循环和for(int i = 0;i < nums.length; i++)
这种常见的有什么区别?
解释:
for(String str : strs)
是 增强型 for 循环(for-each 循环),适用于遍历数组或集合中的每个元素,代码更简洁且没有下标操作。for(int i = 0; i < nums.length; i++)
是传统的 for 循环,需要使用下标访问数组元素,适用于需要知道元素索引或在遍历过程中修改下标的场景。- 举例说明:
String[] words = {"hello", "world"}; // 增强 for 循环 for (String word : words) { System.out.println(word); } // 传统 for 循环 for (int i = 0; i < words.length; i++) { System.out.println(words[i]); }
两种方式都可以遍历数组,但 for-each 写法更加简洁且避免了索引越界的风险。
[NOTE] 问题3
问题3:Arrays.sort(array);
这句的用法是java自带的吗?对字符数组排序是怎么做到的?转换为ASCII码吗?
Arrays.sort(array);
是 Java 自带的工具方法,位于 java.util.Arrays 类中。- 当对字符数组排序时,底层会使用一种高效的排序算法(如双轴快速排序)对字符的 Unicode 值进行比较。
- 对于英文小写字母,它们的 Unicode 值与 ASCII 码是一致的,所以排序时会按照字母的自然顺序(例如 a, b, c, …)排序。
- 示例说明:
char[] letters = {'c', 'a', 'b'}; Arrays.sort(letters); System.out.println(new String(letters)); // 输出:"abc"
[NOTE] 问题4
问题4:String key = new String(array);
为什么将排序后的字符数组转换回字符串,用作哈希表中的键之后,所有字母异位词将共享同一个键?
解释:
- 将字符数组排序后,相同字母构成的不同单词(即字母异位词)会得到相同的排序结果。例如,“eat”、“tea”、“ate” 排序后都得到
"aet"
。- 用排序后的字符串作为键,就可以把这些异位词归为同一组,因为它们具有相同的键。
- 示例说明:
String word1 = "eat"; String word2 = "tea"; // 将两个单词转换为字符数组并排序 char[] arr1 = word1.toCharArray(); char[] arr2 = word2.toCharArray(); Arrays.sort(arr1); Arrays.sort(arr2); String key1 = new String(arr1); // "aet" String key2 = new String(arr2); // "aet" // 因为 key1 和 key2 相同,所以可以用相同的键存储在 map 中
[NOTE] 问题5
问题5:List<String> list = map.getOrDefault(key, new ArrayList<String>());
这句中new ArrayList<String>()
在java中是如何使用的?为什么选择这样的数据类型?
解释:
new ArrayList<String>()
表示创建一个新的空的字符串列表。- 在
map.getOrDefault(key, new ArrayList<String>())
中,如果 map中不存在 key对应的值,就返回一个新的空 ArrayList。- 选择 ArrayList 是因为它实现了 List接口,具备动态调整大小、方便添加元素的特点,非常适合存储多个字符串。
- 示例说明:
// 假设 map 中还没有 key 为 "aet" 的条目 List<String> list = map.getOrDefault("aet", new ArrayList<String>()); list.add("eat"); // 将 "eat" 加入列表中 map.put("aet", list);
[NOTE] 问题6
问题6:return new ArrayList<List<String>>(map.values());这句中new ArrayList<String>()
在java中是如何使用的?为什么选择这样的数据类型?
解释:
map.values()
返回的是一个Collection<List<String>>
,包含了 map 中所有的值(每个值是一个字符串列表)。- 使用
new ArrayList<List<String>>(map.values())
将这些值转换为一个新的 ArrayList。- 选择 ArrayList 是因为返回类型要求是 List<List>,ArrayList 是 List 接口的常用实现,便于后续遍历和使用。
- 示例说明:
// map.values() 返回所有分组的列表 Collection<List<String>> groups = map.values(); // 将 Collection 转换成 ArrayList,以便返回 List<List<String>> result = new ArrayList<>(groups); return result;
总结回顾
ArrayList
在这道题目中出现了ArrayList
,借机回顾一下用法:
ArrayList
是一个动态数组,适用于需要频繁添加、删除和随机访问元素的场景。它提供了简单易用的 API,使得我们可以方便地操作集合数据。由于底层基于数组实现,访问速度快,但在中间插入或删除元素时可能需要移动数据,因此在设计程序时需要考虑操作的频率和数据量。
-
声明与初始化
-
声明一个
ArrayList
时,通常使用泛型指定存储的数据类型。例如:
ArrayList<String> list = new ArrayList<>();
-
也可以使用接口类型声明:
List<String> list = new ArrayList<>();
-
-
添加元素
-
在列表末尾添加元素:
list.add("Apple");
-
在指定位置插入元素:
list.add(0, "Banana");
-
-
访问元素
-
根据下标获取元素:
String fruit = list.get(0);
-
获取列表的大小:
int size = list.size();
-
-
修改元素
- 替换指定位置的元素:
list.set(1, "Cherry");
- 替换指定位置的元素:
-
删除元素
-
根据下标删除元素:
list.remove(0);
-
删除指定对象:
list.remove("Cherry");
-
-
遍历列表
-
使用增强型 for 循环:
for (String item : list) { System.out.println(item); }
-
使用传统 for 循环:
for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); }
-
-
常用其他方法
-
判断列表是否为空:
list.isEmpty();
-
清空列表:
list.clear();
-
检查是否包含某个元素:
list.contains("Apple");
-
将列表转换为数组:
String[] array = list.toArray(new String[0]);
-