一、Hash
散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。 这个映射函数称做散列函数,存放记录的数组称做散列表。
- 实现Hash算法的关键:实现hash算法 、解决hash冲突
1.Hash函数
首先来说hash函数,java中对象都已一个hashCode()方法,那为什么还需要hash函数呢?hashCode是在jdk中是有符号int类型,这个一个很大的范围,如果散列表的数组能覆盖所有int值的话,就不需要hash函数了,当然内存不允许我们维护这么大的散列表。这时我们需要hash函数将原始hashCode映射到一个很小的数组上去。意思就是将超大超长或不定长的整形数据转换为唯一(理想情况,对于不同对象hash值应该不相同)的定长的hash值,常见的做法是取模法,也是jdk中的实现方式。
- HashMap的hashCode实现:
1 static final int hash(Object key) { 2 int h; 3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 4 } 5 6 static int indexFor(int h, int length) { 7 return h & (length-1); 8 }
第一个hash函数有人称之为“扰动函数”,第二个indexFor函数在jdk8中去掉了,函数内的代码合并到了putVal中,个人认为这两个函数合并起来是一个完整的hash函数。
h & (length-1) 这段代码的作用其实就是取模,假设数组初始化长度为16,那么length-1的结果为15,对应二进制为00001111,如果我们有一个大小为20的key,对应二进制为00010100,与运算后结果为00000100,对应十进制为4。
这里数组的长度必须为2的次幂。由于对key进行了取模运算,所以我们知道当length=16的时候,我们会舍弃调掉key高位的值,只保留了低4位。本来int是32位,只是用低4位冲突是不是太容易发生了?
所以第一个“扰动函数”的作用出现了,这个函数将key本身高16和低16位做了异或运算。
尽管实现了如此有效的散列算法,但只是将不同对象之间hash碰撞的概率降低了,还是不能完全保证不发生hash冲突,因此要继续使用hash表的优点就要解决hash冲突的问题。
二、解决Hash冲突
1.开放定址法(线性探测,二次探测,伪随机探测)
用开放定址法解决冲突的做法是:当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探查到开放的 地址则表明表中无待查的关键字,即查找失败。
当哈希表越来越满时聚集越来越严重,这导致产生非常长的探测长度,后续的数据插入将会非常费时。通常数据超过三分之二满时性能下降严重,因此设计哈希表关键确保不会超过这个数据容量的一半,最多不超过三分之二。
- 用开放定址法建立散列表时,建表前须将表中所有单元(更严格地说,是指单元中存储的关键字)置空。
- 空单元的表示与具体的应用相关。
按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、线性补偿探测法、随机探测等。
(1)线性探查法(Linear Probing)
- 该方法的基本思想是将散列表T[0..m-1]看成是一个循环向量,若初始探查的地址为d(即h(key)=d),则最长的探查序列为:
d,d+l,d+2,…,m-1,0,1,…,d-1
即:探查时从地址d开始,首先探查T[d],然后依次探查T[d+1],…,直到T[m-1],此后又循环到T[0],T[1],…,直到探查到T[d-1]为止。
- 探查过程终止于三种情况:
①若当前探查的单元为空,则表示查找成功(若是插入则将key写入其中);
②若当前探查的单元中含有key,则查找成功,但对于插入意味着失败;
③若探查到T[d-1]时仍未发现空单元,则无论是查找还是插入均意味着失败(此时表满,需要扩容)。
- 利用开放地址法的一般形式,线性探查法的探查序列为:
hi=(h(key)+i)%m 0≤i≤m-1 //i=1
- 用线性探测法处理冲突,思路清晰,算法简单,但存在下列缺点:
①哈希表容量不能完全利用,并且扩容将会是灾难的,需要删除以前标记过的元素并需要从新计算所有元素的位置,在频繁的删除和插入时效率变得很低。
②按上述算法建立起来的哈希表,删除工作非常困难。假如要从哈希表 HT 中删除一个记录,按理应将这个记录所在位置置为空,但我们不能这样做,而只能标上已被删除的标记,否则,将会影响以后的查找。
③线性探测法很容易产生堆聚现象。所谓堆聚现象,就是存入哈希表的记录在表中连成一片。按照线性探测法处理冲突,如果生成哈希地址的连续序列愈长(即不同关键字值的哈希地址相邻在一起愈长),则当新的记录加入该表时,与这个序列发生冲突的可能性愈大。因此,哈希地址的较长连续序列比较短连续序列生长得快,这就意味着,一旦出现堆聚(伴随着冲突),就将引起进一步的堆聚。
(2)线性补偿探测法
线性补偿探测法的基本思想是将线性探测的步长从 1 改为 Q ,即将上述算法中的 hi=(h(key)+i)%m改为:hi=(h(key)+Q)%m,这个Q是根据一定的增长率变化的(1、4、9...),这样使得数据分布的足够散乱,不容易出现聚堆现象,而且要求 Q 的变化能使全表得到完整的扫描,以便能探测到哈希表中的所有单元(当然还有其他的线性在散列算法规则,这里只讨论该种方式的再散列)。
(3)随机探测
随机探测的基本思想是将线性探测的步长从常数改为随机数,即令:hi=(h(key)+RN)%m ,其中 RN 是一个随机数。在实际程序中应预先用随机数发生器产生一个随机序列,将此序列作为依次探测的步长。这样就能使不同的关键字具有不同的探测次序,从而可以避免或减少堆聚。基于与线性探测法相同的理由,在线性补偿探测法和随机探测法中,删除一个记录后也要打上删除标记。
2.链地址法(拉链法)
拉链法解决冲突的做法是将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于 1,但一般均取α≤1。
- 与开放定址法相比拉链法的优点
①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在 用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。
- 拉链法的缺点
当发生hash冲突时,需要生成链表由此需要额外占用空间,并且需要花一定的时间和精力维护冲突链表。在扩容的时候需要把所有元素进行重新hash并分配地址,算法较为复杂繁琐。
3.再哈希(了解)
再hash法,就是算hashcode的方法不止一个,一个要是算出来重复啦,再用另一个算法去算。使用一定的算法逻辑的到一种在当前情况不会发生hash冲突的hash算法。
4.建立公共溢出区(了解)
建立一个公共溢出区域,就是把冲突的都放在另一个地方,不在表里面。具体实现不做探讨了(不常用)。
原文地址:https://www.cnblogs.com/tag6254/p/9416946.html