生活中的惊喜,是我主动发现的,还是惊喜来找我的呢?
文章目录
前言
HashMap底层数据结构与hash冲突是经典面试题,本文会详细介绍这两个问题.
1. HashMap概念
HashMap是通过某种函数,根据元素的值作为关键码,进而计算出元素的地址.由于哈希表的值与地址是一一对应的关系,不需要任何比较,所以哈希表的查找时间复杂度为O(1).
1.1 哈希函数
hash(key) = key % capacity.
key为元素值,capacity为哈希表容量.
如下图,先计算元素的地址,元素1的地址为1%10 = 1.其余元素同理.
2. 哈希冲突
根据哈希函数hash(key) = key % capacity.1%10 = 4, 44 % 10 = 4.所以,这两个元素,根据哈希函数计算出的地址值为同一处地址.这种情况为哈希冲突.
2.1 冲突避免-负载因子调节.
散列表的负载因子定义为: α = 表中元素个数/表的总长度.
负载因子越大,产生冲突的概率就越大.对于开放地址法,负载因子要控制在0.7-0.8以下,超过0.8,查表时不命中.因此,对于采用开放地址法的hash库,如JAVA系统库限制了负载因子为0.75.一旦超过了这个值,需要扩大哈希表的容量.
3. 冲突解决-开散列与闭散列
3.1 闭散列
闭散列也就是开放地址法,发生哈希冲突时,如果哈希表还有空余位置,就可以把这个元素放在后面的空余位置,那么怎么确定这个空余位置呢?
3.1.1 线性探测
发生哈希冲突时,就从冲突位置向后探测,直到找到下一个空余位置,把这个元素放在这个位置.
如下图,填入44时,与4发生了哈希冲突,就从元素4所在位置向后探测,直到找到了空余位置8,就把44放在这个空余位置1.
注意,在使用这个方法时,删除前面的元素不可直接删除,可能会影响到后面的元素,所以,删除元素只能采用伪删除的元素,例如将元素值改为根本不可能出现的值.
3.1.2 二次探测
线性探测有一个问题,是元素会很密集的堆积在一起.为了避免这个问题,引入了二次探测.
当发生哈希冲突时 : 地址 = (H0 + i2) % m或者 (H0 - i2) % m.H0为通过哈希函数计算出来的值,m为哈希表容量,i为1,2,3,4.
如下图
具体步骤为,填入19时,先用哈希函数计算出第一个地址,H0 = 19%11 = 8,发生了哈希冲突
将i = 1,和新地址H0 = 8带入上式 (H0 + i2) % m,(8 + 1)%11 = 9,再次发生哈希冲突.
再将i= 1和H0 = 8带入(H0 - i2) % m,(8-1)%11 = 7,再次发生哈希冲突.
将i = 2,H0 = 8,带入 (H0 + i2) % m,(8 + 4)%11 = 1,发生哈希冲突.
将i = 2,H0 = 8,带入 (H0 - i2) % m,(8 - 4)%11 = 4,发生哈希冲突.
将i = 3,H0 = 8, 带入(H0 + i2) % m,(8+9)%11 = 6,未发生哈希冲突,填入.
3.2 开散列
开散列是将通过哈希函数计算出来的地址值相同的元素,都用单链表串在一起,将链表表头存放在哈希表中,每一个链表成为一个哈希桶.如下图所示.
若冲突仍然十分严重,就将哈希表的每个元素都升级成哈希表,就将发生冲突的元素都放在一个哈希表中.