由一道算法题引发的思考:HashMap的key能否为数组?

先说结论: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 是否与要插入的键值对具有相同的键。具体来说,它进行了以下判断:

  1. p.hash == hash:比较当前节点 p 的哈希值与待插入键值对的哈希值是否相等。这是为了确保它们在哈希表中的索引位置相同。

  2. ((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());
}
相关推荐
莹雨潇潇2 分钟前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
杨哥带你写代码21 分钟前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端
我是哈哈hh43 分钟前
专题十_穷举vs暴搜vs深搜vs回溯vs剪枝_二叉树的深度优先搜索_算法专题详细总结
服务器·数据结构·c++·算法·机器学习·深度优先·剪枝
郭二哈1 小时前
C++——模板进阶、继承
java·服务器·c++
A尘埃1 小时前
SpringBoot的数据访问
java·spring boot·后端
Tisfy1 小时前
LeetCode 2187.完成旅途的最少时间:二分查找
算法·leetcode·二分查找·题解·二分
yang-23071 小时前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
沉登c1 小时前
幂等性接口实现
java·rpc
代码之光_19801 小时前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端
Mephisto.java1 小时前
【力扣 | SQL题 | 每日四题】力扣2082, 2084, 2072, 2112, 180
sql·算法·leetcode