先说结论:HashMap的key不能为数组
那道算法题
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例 1:
less
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
示例 2:
lua
输入: strs = [""]
输出: [[""]]
示例 3:
lua
输入: strs = ["a"]
输出: [["a"]]
提示:
1 <= strs.length <= 104
0 <= strs[i].length <= 100
strs[i]
仅包含小写字母
解题思路
首先要确定怎么判断两个字符串是字母异位词,我的思路是把数组看为一个包含26个字母的哈希表,比如a就就对应数组的第一位,b就对应数组的第二位,以此类推。然后遍历字符串的每一个char,将字母对应的数组下标++。
比如说字符串"ate"就对应数组[1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],这个数组同时也表示这个字符串包含一个'a',一个'e',一个't'。然后在把这个数组作为HashMap的key,把这个字符串加入到对应的Value里面,最后,取出map的集合的值就是我们的答案。
arduino
public static List<List<String>> groupAnagrams1(String[] strs) {
//新建一个 Map<String, List<String>>,key为判断单词为字母异位词的标志点,value为字母异位词相同的集合
Map<int[], List<String>> map = new HashMap<>();
//取出字符串的集合的每个字符串
for (String str : strs) {
//新建一个数组作为哈希表
int[] ints = new int[26];
//遍历字符串的每个字母
for (int i = 0; i < str.length(); i++) {
//字母对应的数组下标++
//str.charAt(i) - 'a'比较巧妙,我们并不需要去记住字母的ASCII值,只需要将字母减去'a'就可这知道它在数组中的 位置,比如遍历到的字母为'a',则'a'-'a'=0,那么ints[0]++。比如遍历到的字母为'b',则'a'-'b'=0,那么 ints[1]++。
ints[str.charAt(i) - 'a']++;
}
//把字符串对应的数组作为Key从map集合中取数据,如果map已经有对应的Key了则去出该List<String>,没有则new一个
List<String> list = map.getOrDefault(ints, new ArrayList<>());
//把取出来的list集合假设这个字符串
list.add(str);
//把加上了这个字符串的新的集合放回map集合
map.put(ints, list);
}
//返回map集合的所有value就是我们的答案
return new ArrayList<>(map.values());
}
上面的思路乍一看是没有什么问题的,但是最后提交的时候却提示错了:情况如图所示
我们可以看到,我们的输出结果中并不会把字母异位词放入同一个集合中,而我调试打印了"eat"和"tea"遍历后的数组,发现它们的值是一样的,所以问题就出在这里
我们把数组作为key放入map集合中,但是map集合会把相同的数组认为是不同的key,所以也就引出来了我们这篇文章要讨论的问题:HashMap的key能否为数组?
答案是不能的,那原因是什么呢?我们不妨先探讨一下下面这个问题
HashMap的put过程
ini
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//计算插入位置的索引 i,并检查对应位置是否为空。如果为空,直接在该位置创建新的节点并插入。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//检查当前位置的节点是否是要插入的节点。如果是,则直接将当前节点赋值给 e。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//如果当前节点是树节点(表示该位置已经形成树),则调用 putTreeVal 方法进行插入。
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果当前位置的节点不是要插入的节点且不是树节点,则进入循环处理链表冲突。
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//检查链表中是否存在相同键的节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
.....
}
......
return null;
}
上面的代码有点多,我们直接看到最重要的一句
vbnet
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
这条判断语句用于检查当前位置的节点 p
是否与要插入的键值对具有相同的键。具体来说,它进行了以下判断:
-
p.hash == hash
:比较当前节点p
的哈希值与待插入键值对的哈希值是否相等。这是为了确保它们在哈希表中的索引位置相同。 -
((k = p.key) == key || (key != null && key.equals(k)))
:比较当前节点p
的键与待插入键值对的键是否相等。这里使用了一个临时变量k
来存储当前节点的键,以避免多次调用p.key
。判断条件包括两个部分:(k = p.key) == key
:检查引用相等性,即判断它们是否指向同一个对象。(key != null && key.equals(k))
:如果引用不相等,就使用equals
方法进行键的比较,以确保它们的内容相等。
所以说判断判断key是否相等首先进行了HashCode的判断,而在 Java 中,数组的 hashCode
方法是继承自 Object
类的,它的默认实现是基于数组对象的内存地址计算的。具体来说,hashCode
返回的是一个整数,该整数由数组对象的内部地址信息生成。
这意味着如果两个数组是相同类型、相同长度、相同类型元素的数组,但它们是两个不同的对象,它们的 hashCode
也会不同。这是因为它们的内部地址是不同的。比如下面这段代码:
ini
int[] array1 = {1, 2, 3};
int[] array2 = {1, 2, 3};
System.out.println(array1.hashCode()); // 输出:一般情况下,这两个输出值是不同的
System.out.println(array2.hashCode());
System.out.println(Arrays.equals(array1, array2)); // 输出:true,因为元素相同
这是输出结果,我们可以看到它们的HashCode是不一样的。这也就是HashMap的key不能为数组,数组的hashCode()方法没有被重写,只是单纯地根据内存地址来计算hashcode。
解决方法
第一种:Arrays.toString(ints);
既然数组的hashCode是是继承自 Object
类的,没有被重写,那我们就不用直接用数组来作为key,而是使用Arrays.toString(ints)转化为String来作为key。
arduino
public static List<List<String>> groupAnagrams1(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String str : strs) {
int[] ints = new int[26];
for (int i = 0; i < str.length(); i++) {
ints[str.charAt(i) - 'a']++;
}
String s = Arrays.toString(ints);
List<String> list = map.getOrDefault(s, new ArrayList<>());
list.add(str);
map.put(s, list);
}
return new ArrayList<>(map.values());
}
第二种:使用集合
第二种方法思路其他与前面的一致,还是不用数组作为key,而是创建容量为26且每一位数值为0的集合来当成数组
arduino
public static List<List<String>> groupAnagrams2(String[] strs) {
Map<List<Integer>, List<String>> map = new HashMap<>();
for (String str : strs) {
List<Integer> ints = new ArrayList<>(Collections.nCopies(26, 0));
for (int i = 0; i < str.length(); i++) {
ints.set(str.charAt(i) - 'a', ints.get(str.charAt(i) - 'a') + 1);
}
List<String> list = map.getOrDefault(ints, new ArrayList<>());
list.add(str);
map.put(ints, list);
}
return new ArrayList<>(map.values());
}