哈希表(Hash Table) 也称散列表,底层数据结构为一维数组存储K-V形式的数据。其基本思想是:通过key的哈希值来映射到数组中的索引位置,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数。散列函数非常重要,它是决定哈希表性能的关键。优秀的散列函数能够尽可能地减少哈希冲突,但是哈希冲突无法避免。可以参考HashMap底层数据结构。
jdk1.8的HashMap底层数据结构为数组+链表+红黑树。
hash冲突(哈希碰撞): 对不同的关键字可能得到同一散列地址,即key1≠key2,而hash(key1) = hash(key2),这种现象称(冲突)碰撞。
其hash函数如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
调用key.hashCode()无符号右移16位的hashcode后得到的值做异或运算
常见的hash函数:
1.除余法
顾名思义,除余法就是用关键码x除以M(往往取散列表长度),并取余数作为散列地址。除余法几乎是最简单的散列方法,散列函数为: h(x) = x mod M。
2.乘余取整法
使用此方法时,先让关键码key乘上一个常数A (0< A < 1),提取乘积的小数部分。然后,再用整数n乘以这个值,对结果向下取整,把它做为散列的地址。散列函数为: hash ( key ) = _LOW( n × ( A × key % 1 ) )。 其中,“A × key % 1”表示取 A × key 小数部分,即: A × key % 1 = A × key - _LOW(A × key), 而_LOW(X)是表示对X取下整
3.平方取中法
由于整数相除的运行速度通常比相乘要慢,所以有意识地避免使用除余法运算可以提高散列算法的运行时间。平方取中法的具体实现是:先通过求关键码的平方值,从而扩大相近数的差别,然后根据表长度取中间的几位数(往往取二进制的比特位)作为散列函数值。因为一个乘积的中间几位数与乘数的每一数位都相关,所以由此产生的散列地址较为均匀。
如关键字Key=3632,则3632^2 = 13191424。若表长为1000,则可以取第4~6位为哈希地址,即hash(3632) = 914
4.数字分析法
假设关键字集合中的每个关键字都是由 s 位数字组成 (u1, u2, …, us),分析关键字集中的全体,并从中提取分布均匀的若干位或它们的组合作为地址。数字分析法是取数据元素关键字中某些取值较均匀的数字位作为哈希地址的方法。即当关键字的位数很多时,可以通过对关键字的各位进行分析,丢掉分布不均匀的位,作为哈希值。它只适合于所有关键字值已知的情况。通过分析分布情况把关键字取值区间转化为一个较小的关键字取值区间。
举个例子:要构造一个数据元素个数n=80,哈希长度m=100的哈希表。不失一般性,我们这里只给出其中8个关键字进行分析,8个关键字如下所示:
K1=61317602 K2=61326875 K3=62739628 K4=61343634
K5=62706815 K6=62774638 K7=61381262 K8=61394220
分析上述8个关键字可知,关键字从左到右的第1、2、3、6位取值比较集中,不宜作为哈希地址,剩余的第4、5、7、8位取值较均匀,可选取其中的两位作为哈希地址。设选取最后两位作为哈希地址,则这8个关键字的哈希地址分别为:2,75,28,34,15,38,62,20。
此法适于:能预先估计出全体关键字的每一位上各种数字出现的频度。
5.折叠法
有时关键码所含的位数很多,采用平方取中法计算太复杂,则可将关键码分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列地址,这方法称为折叠法。
分为:
移位叠加:将分割后的几部分低位对齐相加。
边界叠加:从一端沿分割界来回折叠,然后对齐相加。
负载因子(装填因子): 一般情况下,哈希表的空间必须比节点的集合大,虽然浪费了一定的空间但是换取的查询效率。设哈希表的空间大小为m,存储的节点总数为n,则称a = n/m为哈希表的装填因子(或负载因子)。a越小则发生冲突的可能性就越小,反之a越大哈希表中的已填节点数越多,则在填入节点时发生冲突的可能性越大。一般a取0.65-0.9之间为宜。
扩充: 为什么HashMap默认加载因子是0.75?
static final float DEFAULT_LOAD_FACTOR = 0.75f;
简而言之即加载因子过大哈希冲突可能性越大,HashMap解决哈希冲突采用拉链法即再数组尾部产生链表。而从而降低哈希查找效率如未哈希冲突则查询效率为O(1),产生哈希冲突转链表查询查询效率为**O(n)**效率降低。也就是为什么jdk1.8HashMap(链表的深度大于等于8,数组容量大于等于64时,扩容的时候会把链表转成红黑树,时间复杂度从O(n)变成O(logN) )引入红黑树的原因。
哈希冲突的解决办法:
1.开放地址法:查找一个为空的单元插入,线性探测法,在平方,伪随机
2.链地址法:相同的hash值,使用链表进行相连,使数组存储每一个链表。
3.公共溢出区:建立一个特殊的存储空间,专门存放冲突数据,这个方法适用于数据和冲突较少的情 况
4.再散列法:重新hash
拉链法: