导读:今天看了java里面关于hashmap的相关源码(看了java6和java7),尤其是resize、transfer、put、get这几个方法,突然明白了,为什么我之前考数据结构死活考不过,就差那么一点点。答:代码积累太少了!这段时间,看了java的源码、演变过程等,被虐的很惨,但是,很开心! 本篇文章,主要介绍解决hash算法冲突的方法


一、基本概念

散列表:

hash:a mixture of meat, potatoes, and vegetables cut into small pieces and baked or fried

简单说来,hash就是一组碎片的集合!所以,我们常说的hash函数,即散列函数指:数据元素的键值和存储位置之间建立的对应关系H !而用键值通过散列函数获取存储位置的这种存储方式构造的存储结构称为散列表(hash table),这一映射过程称为散列。如果选定了某个散列函数H及相应的散列表L,则对每个数据元素X,函数值H(X.key)就是X在散列表L中的存储位置,这个存储位置也称为散列地址(可以理解为hashcode)


PS:在理想情况下,应用的散列函数可以使每个键值与散列地址是意义对应的,但在实际应用中,这种情况很少或几乎不会出现。经常出现的问题有:冲突

冲突:如果有散列函数H和键值A、B(A不等于B),但是H(A)=H(B)即算出的散列地址是一样的,这种现象称为冲突!          ——A、B为相对于H的同义词


二、常用的解决hash冲突的方法

2.1,开放地址法

当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定 的关键字,或者碰到一个开放的地址(即该地址单元为空)为止,常用的方法有:线性探测法、二次探测法(解决线性探测的堆积问题)、随机探测法(和二次探测原理一致,不一样的是:二次探测以定值跳跃,而随机探测的散列地址跳跃长度是不定值)

缺点:1,删除工作很困难,假如要从哈希表 HT 中删除一个记录,应将这个记录所在位置置为空,但我们只能标上已被删除的标记,否则,将会影响以后的查找。

2,不易探测到整个散列表的所有空间(线性探测法除外,但线性探测会出现堆积)

2.2,拉链法(链地址法)

将所有关键字为同义词的结点链接在同一个单链表中,【例】设有 m = 5 , H(K) = K mod 5 ,关键字值序例 5 , 21 , 17 , 9 , 15 , 36 , 41 , 24 ,按外链地址法所建立的哈希表如下图所示:


优点:1,处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短。

2,由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况。

3,更易于实现插入和删除


缺点:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

2.3,多重散列法(再哈希法)

这种方法是同时构造多个不同的哈希函数:
    Hi=RH1(key)  i=1,2,…,k
当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生“堆积”,但增加了计算时间。

2.4,公共溢出区法

散列表由两个一维数组组成,一个称为基本表,它实际上就是一个散列表。另外一个称为溢出表。插入首先在基本表上进行,假如发生冲突,则将同义词存入溢出表。这样,可以保证基本表不会发生“堆积”

PS:基本表是不会发生堆积了,那溢出表呢?当进行查找时,查找到溢出表,这是不是又开启了新一轮的冲突解决?


三、总结

再次看了一遍书,对于hash函数有了更深一层的理解。尤其是看了一些代码之后,发现自己真的很low!如上述介绍,综合而言,开放地址法、再哈希法、公共溢出区法都无可避免的多次冲突或堆积的解决或者消费了大量的时间,所以,选择链地址法是相对而言最合适的。

现在,终于解决了一个自己之前的疑惑,为什么hashmap或者hashtable会选择用一个数组和链表的形式来实现?我那时候就在想,为啥不是全用数组呢?我只想到了把数据放进去,却没有去想怎么把数据拿出来用、综合效率问题!

posted on 2017-01-04 16:18  何红霞  阅读(1371)  评论(1编辑  收藏  举报