在hashCode中,为什么是31作为乘数?


问题

在hashCode中,为什么是31作为乘数?


一、hashCode的源码部分

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

其中,31作为乘数,在每次循环中参与运算。

二、解释

  1. 31是一个质数或者说是一个奇素数,如果选择偶数作为乘数,可能会导致运算过程中数据溢出。
  2. 在二进制中,2^5=32,即31 * i = (i << 5) - i,在乘积计算中可以提升位移性能,而且目前的JVM虚拟机自动支持此类的优化。

三、实验

实验一:碰撞概率的计算

探究不同乘数对碰撞概率的影响,实验中使用的乘数有2, 3, 5, 7, 17, 31, 32, 33, 39, 41, 199。

数据集:
10万个单词的字典表,字典部分内容如下所示:
在这里插入图片描述

hashCode函数:

// hashCode函数,指定乘数进行hashcode计算
   public static Integer hashCode(String str, Integer multiplier) {
        int hash = 0;
        for (int i = 0; i < str.length(); i++) {
            hash = multiplier * hash + str.charAt(i);
        }
        return hash;
    }

Hash碰撞概率的计算:
统计出总的hash的个数和所有不同的hash值的个数,使用前者减去后者,得到碰撞的hash值的个数。

     // 计算Hash碰撞概率
	private static RateInfo hashCollisionRate(Integer multiplier, List<Integer> hashCodeList) {
        int maxHash = hashCodeList.stream().max(Integer::compareTo).get();
        int minHash = hashCodeList.stream().min(Integer::compareTo).get();

        int collisionCount = (int) (hashCodeList.size() - hashCodeList.stream().distinct().count());
        double collisionRate = (collisionCount * 1.0) / hashCodeList.size();

        return new RateInfo(maxHash, minHash, multiplier, collisionCount, collisionRate);
    }
// 测试函数
public class ApiTest {

    private Set<String> words;

    @Before
    public void before() {
        "abc".hashCode();
        // 读取文件,103976个英语单词库.txt
        words = FileUtil.readWordList("E:\\xfg_ProjectCode\\interview-master\\interview-03\\103976个英语单词库.txt");
    }

    @Test
    public void test_collisionRate() {
        System.out.println("单词数量:" + words.size());
        List<RateInfo> rateInfoList = HashCode.collisionRateList(words, 2, 3, 5, 7, 17, 31, 32, 33, 39, 41, 199);
        for (RateInfo rate : rateInfoList) {
            System.out.println(String.format("乘数 = %4d, 最小Hash = %11d, 最大Hash = %10d, 碰撞数量 =%6d, 碰撞概率 = %.4f%%", rate.getMultiplier(), rate.getMinHash(), rate.getMaxHash(), rate.getCollisionCount(), rate.getCollisionRate() * 100));
        }
    }
}

实验结果:
在这里插入图片描述

实验结论:
从实验结果中能够观察到,随着质数的乘数的增大,碰撞概率在逐渐减小,当乘数为偶数时,如2和32,碰撞概率依然很大,当乘数增大到31时,碰撞概率已经很小。

实验二:Hash散列分布

探究不同乘数对散列分布的影响,实验中使用的乘数有2, 3, 5, 7, 17, 31, 32, 33, 39, 41, 199。
将2^32分成64份,也就是64个格子,将统计的所有的hashcode放入对应的格子中,观察它们的分布情况。

public static Map<Integer, Integer> hashArea(List<Integer> hashCodeList) {
    Map<Integer, Integer> statistics = new LinkedHashMap<>();
    int start = 0;
    for (long i = 0x80000000; i <= 0x7fffffff; i += 67108864) {
        long min = i;
        long max = min + 67108864;
        // 找出每个格子对应范围内的所有hash值
        int num = (int) hashCodeList.parallelStream().filter(x -> x >= min && x < max).count();
        statistics.put(start++, num);
    }
    return statistics;

// 测试函数
public class ApiTest {
    private Set<String> words;

    @Before
    public void before() {
        "abc".hashCode();
        // 读取文件,103976个英语单词库.txt
        words = FileUtil.readWordList("E:\\xfg_ProjectCode\\interview\\interview-03\\103976个英语单词库.txt");
    }

    @Test
    public void test_hashArea() {
        System.out.println(HashCode.hashArea(words, 2).values());
        System.out.println(HashCode.hashArea(words, 7).values());
        System.out.println(HashCode.hashArea(words, 31).values());
        System.out.println(HashCode.hashArea(words, 32).values());
        System.out.println(HashCode.hashArea(words, 199).values());
    }

}

实验结果:

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 103910, 34, 0, 18, 0, 0, 7, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 2, 0, 0, 0, 0]
[310, 364, 284, 292, 393, 506, 656, 460, 451, 410, 441, 522, 485, 458, 725, 559, 391, 493, 319, 308, 479, 469, 454, 354, 491, 486, 361, 425, 347, 442, 245, 317, 38397, 15253, 286, 371, 869, 1509, 2012, 1049, 1186, 2890, 7992, 6821, 975, 1061, 1467, 1130, 1350, 1200, 482, 291, 319, 301, 241, 245, 324, 239, 260, 272, 433, 317, 266, 471]
[1292, 1065, 1197, 1148, 1217, 978, 1157, 1157, 1203, 1099, 1737, 3011, 2235, 2160, 1612, 2443, 2004, 1941, 3268, 2047, 1792, 1615, 1257, 1316, 1350, 1297, 1234, 1698, 1318, 1137, 1217, 1385, 8237, 8404, 1214, 1205, 1199, 1174, 1092, 1203, 1472, 1047, 1125, 1264, 1317, 1075, 1527, 1231, 1552, 1041, 1201, 1085, 1258, 1153, 1255, 1223, 1289, 1084, 1008, 1440, 1230, 1400, 1107, 1277]
[0, 0, 1607, 1044, 1614, 1559, 1135, 1575, 1664, 2360, 1733, 2642, 1305, 301, 195, 0, 0, 0, 3653, 1745, 3973, 2144, 2987, 1750, 1090, 3674, 2153, 2471, 1390, 391, 579, 0, 6948, 7109, 2489, 2931, 1340, 980, 1016, 2407, 5064, 2291, 3314, 4285, 1138, 351, 658, 0, 0, 0, 2142, 988, 2709, 485, 1919, 587, 547, 2290, 612, 1555, 870, 57, 160, 0]
[1415, 1455, 1617, 1642, 1491, 1418, 1506, 1663, 1694, 1594, 1450, 1428, 1462, 1545, 1693, 1566, 1354, 1413, 1495, 1720, 1648, 1641, 1473, 1386, 1401, 1468, 1548, 1518, 1342, 1409, 1561, 1656, 4110, 1423, 1319, 1327, 1404, 1639, 1579, 1389, 1406, 1452, 1613, 3019, 3373, 3030, 1748, 1471, 1490, 1469, 1457, 1396, 1457, 1644, 1754, 1624, 1503, 1498, 1304, 1507, 1458, 1501, 1486, 1454]

从实验结果可以看到,不同乘数对hashcode的散列分布的影响,使用2或者32,hashcode的散列效果并不好, 很多位置并没有填充hashcode。乘数31和199的散列效果非常好,每个格子都比较均衡地分配了hashcode,但是乘数199并不能使用,因为数据区间的问题会导致数据丢失。

引用 https://bugstack.cn/md/java/interview/2020-08-04-%E9%9D%A2%E7%BB%8F%E6%89%8B%E5%86%8C%20%C2%B7%20%E7%AC%AC2%E7%AF%87%E3%80%8A%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%EF%BC%8CHashCode%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BD%BF%E7%94%A831%E4%BD%9C%E4%B8%BA%E4%B9%98%E6%95%B0%EF%BC%9F%E3%80%8B.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值