HashMap(3)之哈希函数 hash()

HashMap 的哈希函数是怎么设计的?

查看源码可知,HashMap 中的哈希函数是根据对象(也就是 key)的 hashCode 来进行运算得到的:

    /**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

hashCode 是一个 32 位的 int 类型的整数值,它和 Java 中每一个对象(Object)关联,即每一个对象都拥有自己独一无二的 hashCode 值。

而 HashMap 中的哈希函数是先拿到 key 的 hashcode,然后让 hashcode 的高 16 位和低 16 位进行异或操作。如此设计的目的是为了降低哈希碰撞的概率。

为什么哈希函数能降低哈希碰撞的概率?

因为 key.hashCode() 函数调用的是 key 键值类型自带的哈希函数,返回的是 int 型散列值。int 值范围为 -2147483648~2147483647,加起来大概有超过 40 亿的映射空间。

只要哈希函数映射得比较均匀,一般应用是很难出现哈希碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。

假如 HashMap 数组的初始化大小为 16,就需要在使用之前与数组的长度进行取模运算,得到的余数才能用来访问数组下标。

源码中模运算其实就是把散列值和数组长度 -1 做一个"与&"操作,位运算比取余 % 运算要快。

这也正好解释了为什么 HashMap 的数组长度为什么要取 2 的整数幂。因为这样(数组长度 - 1)正好相当于一个 “低位掩码”。"与&"操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度 16 为例,16-1=15。2 进制表示是 0000 0000 0000 0000 0000 0000 0000 1111.。和某个散列值"与&"操作如下,结果就是截取了最低的四位值。

这样是要快捷一些,但是新的问题来了,就算散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。如果散列本身做得不好,分布上成等差数列的漏洞,如果正好让最后几个低位呈现规律性重复,那就更难搞了。

这时候哈希函数的价值就体现出来了,看一下哈希函数的示意图:

右移 16 位,正好是 32 bit 的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。

哈希构造函数

HashMap里哈希构造函数的方法叫:

除留取余法:H(key) = key % p(p <= N),关键字除以一个不大于哈希表长度的正整数 p,所得余数为地址,当然 HashMap 里进行了优化改造,效率更高,散列也更均衡。这是一种最简单、最常用的构造哈希函数的方法,它不仅可以对关键字直接取模,也可以在对关键字进行平方取中等操作之后取模。在使用此方法时,对 p 的选择很重要,一般情况下可以选择 p 为质数。

除此之外,还有这几种常见的哈希函数构造方法:

直接定址法:H(key) = a * key + b。一般取关键字或关键字的某个线性函数值作为哈希地址。如可直接根据 key 来映射到对应的数组位置,例如,1232 放到下标 1232 的位置。由于这种方法得到的地址集合与关键字集合大小相同,所以,对于不同的关键字就不会发生冲突。但是,因为这种方法需要提前确认关键字的范围,且要保证范围不能太大,所以使用场景很少。如:有一个从 1 到 100 岁的人口数量统计表,年龄为关键字,哈希函数取关键字本身,即 H(key) = key。那么,要想统计 26 岁的人口数量,则直接查找表中地址为 26 的桶即可。

数字分析法:取的某些数字(例如十位和百位)作为映射的位置。这种方法通俗理解就是假如关键字都是数字类型的,那么就可以取关键字值的若干位数字组成哈希地址。例如,有 1000 条记录,每个关键字都是 10 位十进制数字。假设经过分析,每个关键字的第 3 位、第 5 位、第 7 位的数字取值分布近似随机分布,则可以使用由关键字的第 3 位、第 5 位、第 7 位数字组成的新数字作为关键字的地址,如H(1496578923) = 958,H(2569874123) = 684。

平方取中法:取 key 平方的中间几位作为映射的位置。通过平方扩大差别,同时由于平方之后的数字中间几位都会与原值有关系,所以这样的化关键字会以较高的频率产生不同的、均匀的哈希地址。例如:将关键字 0100、0110、1010、1001、0111 平方之后得到 0010000、0012100、1020100、1002001、0012321,可以取中间三位 100、121、201、020、123 作为散列地址集。

折叠法:将 key 分割成位数相同的几段,然后把它们的叠加和作为映射的位置。折叠发又可分为分段叠加法和移位叠加法,移位叠加发是将分段之后的每部分低位对齐相加,边界叠加是将奇数段正序偶数段逆序然后相加。关键字位数很多,而且关键字中每一位上数字分布大致均匀时可采用此方法。例如,根据国际标准图书编号(ISBN)建立一个哈希表,如有个国际标准图书编号为 0-442-20586-4 的哈希地址为:

分割后为 04 4220 5864,采用移位叠加得到的地址为:(04 + 4220 + 5864) = 10088;

采用边界叠加得到的地址为:(04 + 0224 + 5864) = 6092。

伪随机数法:选择一个伪随机函数,使用关键字的随机函数值作为该关键字的哈希地址。通常,关键字的长度不相等时采用此方法分配哈希地址。

使用场景:关键字是 ISBN 时可以采用折叠法,关键字是整数类型时可以采用除留取余法、直接定址法和数字分析法;关键字是小数类型时可以使用伪随机数法。

解决哈希冲突

哈希冲突就是两个不同的 key,通过哈希函数计算出来的哈希值相同,这样它们在存储数组中的时候就会发生冲突。为了解决这种冲突,HashMap 底层使用链表来处理,即链式地址法。

链式地址法:在冲突的位置拉一个链表,把冲突的元素放进去

开放定址法:开放定址法就是从冲突的位置再接着往下找,给冲突元素找个空位

找到空闲位置的方法也有很多种:线行探查法:。从冲突的位置开始,依次判断下一个位置是否空闲,直至找到空闲位置;平方探查法。从冲突的位置 x 开始,第一次增加 2 个位置,第二次 4 个位置……直至找到空闲的位置

再哈希法:换种哈希函数,重新计算冲突元素的地址

建立公共溢出区:再建一个数组,把冲突的元素放进去

 

本文参考自:哈希函数的常用构造方法 - 楼兰胡杨 - 博客园

面渣逆袭:HashMap追魂二十三问 - 掘金

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值