HashCode 为什么使用 31 作为乘数?

119 阅读5分钟

这个问题其实指的就是,hashCode 的计算逻辑中,为什么是 31 作为乘数。 在这里插入图片描述 在获取 hashCode 的源码中可以看到,有一个固定值 31,在 for 循环每次执行时 进行乘积计算,循环后的公式如下; s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] Hash 计算函数

  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 碰撞概率计算 想计算碰撞很简单,也就是计算那些出现相同哈希值的数量,计算出碰撞总量即 可。这里的实现方式有很多,可以使用 set、map 也可以使用 java8 的 stream 流 统计 distinct。

/**
     * 计算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);
    }

这里记录了最大 hash 和最小 hash 值,以及最终返回碰撞数量的统计结 果。 单元测试 在这里插入图片描述 以上先设定读取英文单词表中的 10 个单词,之后做 hash 计算。  在 hash 计算中把单词表传递进去,同时还有乘积数;2, 3, 5, 7, 17, 31, 32, 33, 39, 41, 199,最终返回一个 list 结果并输出。  这里主要验证同一批单词,对于不同乘积数会有怎么样的 hash 碰撞结果 如下。

单词数量:103976
乘数 =    2, 最小Hash =          97, 最大Hash = 1842581979, 碰撞数量 = 60382, 碰撞概率 = 58.0730%
乘数 =    3, 最小Hash = -2147308825, 最大Hash = 2146995420, 碰撞数量 = 24300, 碰撞概率 = 23.3708%
乘数 =    5, 最小Hash = -2147091606, 最大Hash = 2147227581, 碰撞数量 =  7994, 碰撞概率 = 7.6883%
乘数 =    7, 最小Hash = -2147431389, 最大Hash = 2147226363, 碰撞数量 =  3826, 碰撞概率 = 3.6797%
乘数 =   17, 最小Hash = -2147238638, 最大Hash = 2147101452, 碰撞数量 =   576, 碰撞概率 = 0.5540%
乘数 =   31, 最小Hash = -2147461248, 最大Hash = 2147444544, 碰撞数量 =     2, 碰撞概率 = 0.0019%
乘数 =   32, 最小Hash = -2007883634, 最大Hash = 2074238226, 碰撞数量 = 34947, 碰撞概率 = 33.6106%
乘数 =   33, 最小Hash = -2147469046, 最大Hash = 2147378587, 碰撞数量 =     1, 碰撞概率 = 0.0010%
乘数 =   39, 最小Hash = -2147463635, 最大Hash = 2147443239, 碰撞数量 =     0, 碰撞概率 = 0.0000%
乘数 =   41, 最小Hash = -2147423916, 最大Hash = 2147441721, 碰撞数量 =     1, 碰撞概率 = 0.0010%
乘数 =  199, 最小Hash = -2147459902, 最大Hash = 2147480320, 碰撞数量 =     0, 碰撞概率 = 0.0000%


Disconnected from the target VM, address: '127.0.0.1:49988', transport: 'socket'

Process finished with exit code 0

以上就是不同的乘数下的 hash 碰撞结果图标展示,从这里可以看出如下信息;

  1. 乘数是 2 时,hash 的取值范围比较小,基本是堆积到一个范围内了,后面内容会看到这块的展示。
  2. 乘数是 3、5、7、17 等,都有较大的碰撞概率
  3. 乘数是 31 的时候,碰撞的概率已经很小了,基本稳定。
  4. 顺着往下看,你会发现 199 的碰撞概率更小,这就相当于一排奇数的茅坑量多,自然会减少碰撞。但这个范围值已经远超过 int 的取值范围 了,如果用此数作为乘数,又返回 int 值,就会丢失数据信息。 Hash 值散列分布 除了以上看到哈希值在不同乘数的一个碰撞概率后,关于散列表也就是 hash, 还有一个非常重要的点,那就是要尽可能的让数据散列分布。只有这样才能减少 hash 碰撞次数。

哈希值分段存放


    public static Map<Integer, Integer> hashArea(Set<String> strList, Integer multiplier){
        List<Integer> hashCodeList = new ArrayList<>();
        for (String str : strList) {
            Integer hashCode = hashCode(str, multiplier);
            hashCodeList.add(hashCode);
        }
        return hashArea(hashCodeList);
    }

    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;
            // 筛选出每个格子里的哈希值数量
            int num = (int) hashCodeList.parallelStream().filter(x -> x >= min && x < max).count();
            statistics.put(start++, num);
        }
        return statistics;
    }

单元测试

 @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());
    }

这里列出我们要统计的乘数值,每一个乘数下都会有对应的哈希值数量 汇总,也就是 64 个格子里的数量。

好了 至此 HashCode 为什么使用 31 作为乘数? 点点关注不迷路 老铁们!!!!!

若是行业老司机想利用工作之余时间接单的可+V:(LI_FLY1314)备注进群** 在这里插入图片描述