一、散列表定义
散列表用的就是数组支持按照下标随机访问的时候,时间复杂度是 O(1) 的特性,它是一种数组的扩展。我们通过散列函数把元素的键值映射为下标,然后将数据存储在数组中对应下标的位置。当我们按照键值查询元素时,我们用同样的散列函数,将键值转化数组下标,从对应的数组下标的位置取数据。
工业级的散列表设计应该从以下几个方面考虑:
(1).设计一个合适的散列函数
(2).定义装载因子阈值,并且设计动态扩容策略
(3).选择合适的散列冲突解决方法
二、散列函数的设计
散列函数设计的好坏,决定了散列表冲突的概率大小,也直接决定了散列表的性能。
1.散列函数设计的基本要求
(1).散列函数计算得到的散列值是一个非负整数;
(2).如果 key1 = key2,那 hash(key1) == hash(key2);
(3).如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。
其中第三点主要是用来解决散列冲突的问题。
2.工业级散列函数的设计
(1).散列函数的设计不能太复杂。过于复杂的散列函数,势必会消耗很多计算时间,也就间接的影响到散列表的性能。
(2).散列函数生成的值要尽可能随机并且均匀分布,这样才能避免或 者最小化散列冲突,而且即便出现冲突,散列到每个槽里的数据也会比较平均,不会出现某个槽 内数据特别多的情况。
(3).常见的散列函数设计方法:数据分析法、直接寻址法、平方取中法、折叠法、随机数法等。
三、散列冲突
1.常用的散列冲突解决方法
(1).开放定址法
定义:开放寻址法的核心思想是,如果出现了散列冲突,我们就重新探测一个空闲位置,将其插入。在开放定址法中,常用的探测插入位置方法有线性探查法、二次探查法、双重散列法等。
性能描述:当散列表中空闲位置不多的时候,散列冲突的概率就会大大提高。为了 尽可能保证散列表的操作效率,一般情况下,会尽可能保证散列表中有一定比例的空闲槽位。用装载因子(load factor)来表示空位的多少。计算公式:
散列表的装载因子 = 填入表中的元素个数 / 散列表的长度
适用场景:当数据量比较小、装载因子小的时候,适合采用开放寻址法。这也是 Java 中的ThreadLocalMap使用开放寻址法解决散列冲突的原因。
(2).链表法
定义:当插入的时候,我们只需要通过散列函数计算出对应的散列槽位,将其插入到对应链表中即可, 所以插入的时间复杂度是 O(1)。
适用场景:基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而 且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树或者跳表来代替链表。可以使得访问效率更高。
注意:
1.在插入或查找的时候,计算Key被映射到桶的位置:
int index = hash(key) & (capacity - 1)
hash()扰动函数计算的值和hash表当前的容量减一,做按位与运算。
这一步,为什么要减一,又为什么要按位与运算?
因为A % B = A & (B - 1),当B是2的指数时,等式成立。
本质上是使用了「除留余数法」,保证了index的位置分布均匀。
2.为什么HashMap的数组长度必须是2的整次幂?
数组长度是2的整次幂时,(数组长度-1)正好相当于一个**“低位掩码”**,“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。
以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111。“与”操作的结果就是截取了最低的四位值。也就相当于取模操作。
3.一致性哈希算法
http://www.zsythink.net/archives/1182
参考:1、极客时间《数据结构与算法之美》