java基础——哈希冲突

转发http://baijiahao.baidu.com/s?id=1586706502510642738&wfr=spider&for=pc

在Java编程语言中,最基本的结构就是两种,一种是数组,一种是模拟指针(引用),所有的数据结构都可以用这两个基本结构构造,HashMap也一样。

HashMap的底层实现原理:
这里写图片描述
在JAVA中,每个对象都有一个散列码,它是由Object类的hashCode()方法计算得到的(当然也可以覆盖Object的hashCode())。而我们可以在散列码的基础上,定义一个哈希函数,再对哈希函数计算出的结果求余,最终得到该对象在哈希表的位置。

HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置。当程序执行 map.put(String,Obect)方法 时,系统将调用String的 hashCode() 方法得到其 hashCode 值——每个 Java 对象都有 hashCode() 方法,都可通过该方法获得它的 hashCode 值。得到这个对象的 hashCode 值之后,系统会根据该 hashCode 值来决定该元素的存储位置。

哈希冲突的产生及解决办法:

1、产生冲突

这里写图片描述
上图就是一个散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

当向HashMap中put数据的时候,首先要判断当前确定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那么新值覆盖原来的旧值,并返回旧值。

如果存在相同的hashcode,那么他们确定的索引位置就相同,这时判断他们的key是否相同,如果不相同,这时就是产生了Hash冲突。

2、解决冲突

HashMap里面没有出现hash冲突时,没有形成单链表时,hashmap查找元素很快,get()方法能够直接定位到元素,但是出现单链表后,单个bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。

当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。
这里写图片描述
Hashmap里面的bucket出现了单链表的形式,散列表要解决的一个问题就是散列值的冲突问题,通常的解决方法如下:

1、链地址法

它有一个桶的概念:对于Entry数组而言,将相同hash值的对象组织成一个链表放在hash值对应的槽位。在链表中的每个元素才是真正的。而一个链表,就是一个桶!因此HashMap最多可以有Entry.length个桶。

2、开放地址法

开放定址法有两种处理方式:一种是线性探测,另一种是平方探测。

线性探测:依次探测冲突位置的下一个位置。如,在哈希表的位置2处发生了冲突,则探测位置3处是否被使用了,若被使用了,则探测位置4……直至下一个被探测的位置为空(意味着还有位置可以插入元素—插入成功)或者探测了N-1(N为哈希表的长度)个元素又回到了原始的冲突位置处(意味着已经没有位置可供新元素插入了—插入失败)

因此,插入一个元素时,最坏情况下的时间复杂度为O(N),因为它有可能探测了N-1个元素!

平方探测:以平方大小来递增下一次待探测的位置。如,在哈希表位置2处发生了冲突,则探测 (1^2=1)位置3(2+1),若位置3被使用了,则探测(2^2=4) 位置6(2+4),若位置6被使用了,则探测(3^2=9)位置11(2+9=11)……平方探测法有一个特点:对于任何一个给定的素数N(假设哈希表的长度设置为素数),当计算( h(k) + i ^2 ) MOD N 时,随着 i 的增长,得到的结果是循环的。

因此,当平方探测重复探测了某一个位置时,说明探测失败即已经没有位置可供新元素插入了,尽管此时哈希表并没有满。

平方探测是跳着探测的,它忽略了一些位置,而这些位置可能是空的。即在哈希表仍未满的情况下,已经不能再插入新元素了

最坏情况下,平方探测需要检测 N/2个位置,因此插入一个元素的最坏时间复杂度为O(N)。

3、再散列法

建立多个hash函数,若是当发生hash冲突的时候,使用下一个hash函数,直到找到可以存放元素的位置。

4、建立公共溢出区

将哈希表分为基本表和溢出表,将与基本表发生冲突的元素放入溢出表中。

底层的hashMap是由数组和链表来实现的,就是上面说的链地址法。首先当插入的时候,会根据key的hash值然后计算出相应的数组下标,计算方法是index = hashcode%table.length,(这个下标就是上面提到的bucket),当这个下标上面已经存在元素的时候那么就会形成链表,将后插入的元素放到尾端,若是下标上面没有存在元素的话,那么将直接将元素放到这个位置上。

当进行查询的时候,同样会根据key的hash值先计算相应的下标,然后到相应的位置上进行查找,若是这个下标上面有很多元素的话,那么将在这个链表上一直查找直到找到对应的元素。

关于Hash的更多问题

1、哈希过程为什么需要先根据hashCode得到一个值(又称散列码),然后再对该值求余呢?

在JAVA中,Object类的hashCode()方法返回的是由调用对象的内存地址导出的一个值,也即,当没有覆盖Object类中的equals() 和 hashCode()时,只有当两个对象的内存地址一样时,才认为两个对象是相等的。这显然不符合实际情况,比如Person类有 String id、String name…..显然在现实中是根据id(身份证)不同来判断两个人不同。因此,需要进一步根据hashCode()值来封装(如上面的 hash(Object k)方法),返回一个合理的散列码。

2、那为什么又需要对得到的散列码求余呢?

底层是用数组来存储的,而我们得到的散列码可能很大(事实上散列码的范围非常广)而内存是有限的,不能分配为数组分配一块很大很大的空间,因此,存储的数组空间相对较小。从而需要把所有的散列码都 “约束” 到这个有效的数组空间中。—-这也是导致冲突的根源

3、为什么使用HashMap查找是O(1)呢?

T value = hashmap.get(key)

①get(key)时,一步计算出该key所对应的底层数组array的 index (相当于上面 hash(Object k ) 和 indexFor(int h, int length) 这两个函数完成的功能)

②value = array[index]

因此,就认为查找的复杂度为O(1)。

4、HashMap中的两个变量及作用

int threshold:当HashMap中的元素个数超过threshold时,就会重新调整哈希的大小。

float loadFactor:loadFactor 默认是0.75,指定threshold,一般情况下,哈希表的大小乘以0.75等于threshold。

猜你喜欢

转载自blog.csdn.net/u010843421/article/details/81181280