哈希表解决冲突的办法主要有两种:
开放寻址法:(open addressing)
下面介绍其中一种,叫线性探测(linear probing)(ThreadLocalMap)
插入:
当插入遇到冲突的时候,这种方法会一直往后面找,知道遇到空闲的位置才把值插入到相应位置。
查找:
查找的时候会先哈希,得到哈希值以后就去相应的下标找数据,如果下标的为位置和你输入的key是一致的,则找到了,如果不一致则继续往下找,知道找到为止。
删除:
当删除的时候,首先要找到相应的值,然后把那个位置标记成deleted。如果不标记成deleted而是把那个位置标记空的话,那有可能下次查找的时候会出现错误,本来有的会找不到。
链表法:(chaining)(LinkedHashMap)
因为每个槽中存储的都是对应数据的链表,所以当出现冲突的时候,我们只需要在当前位置的链表后面增加一个元素即可。
装载因子和动态扩容:
装载因子的大小为 factor = n / m,其中n为当前哈希表有多少个元素,m为哈希表的总的容量。
当装载因子超过事先预设好的阈值后,哈希表就会启动动态扩容。动态扩容有两种方式,一种是一次性扩容,还有一种是分散性扩容。
一次性扩容:
当插入一个新的数据的时候,如果装载因子超过了设定的阈值,则启动动态扩容,这个时候会新申请一个原来容量2倍的哈希表,然后把原来哈希表里面的数据全部重新哈希放到新的哈希表里面去,这样的时间复杂度是O(N)。如果对于性能要求很高的场景,显然这种方式是不合适的。
分散性扩容:
申请一个原来2倍大小的哈希表,把新的数据插入到新的哈希表中,再从老的哈希表里选一个出来插入新的哈希表,直到所有老的哈希表里的数据全部搬到新的哈希表里。对于查找操作来说,先查找新的哈希表再找老的哈希表。这样把动态扩容的时间分摊到每一次的插入操作里,就不会出现时间复杂度为O(N)的情况。可以满足性能要求高的场景。
开放寻址法和链表法优缺点对比:
开放寻址法的缺点:
开放寻址法适合装载因子小,且数据量比较小的场景,因为当装载因子过大的话哈希表的冲突的概略会很大,所以每次查找时间复杂度在极端情况下会退化到O(N)。开放寻址发是开辟一块连续的内存空间,如果数据量很大的话,对内存的利用率不高。
开放寻址发优点:
因为它是连续的一块内存,所以对cpu缓存非常友好,而且这要会对数据结构的序列化有很大的好处。
链表法缺点:
因为里面存储着指针,而且是用链表存储的,所以它不是连续的一块内存,所以会对序列化不利,而且对cpu缓存不友好。
链表法优点:
当出现冲突的时候,只是在链表后面添加或查找元素,在极端的情况下,不用搜索整个哈希表来查找元素,所以适合用来存储大量数据。而且利用链表来存储数据对内存的利用率很高。在某些情况下链表可以该用成红黑树,跳表的数据结构,更进一步加快了插入查找的效率。
如何设计一个好的哈希表:
对于设计一个好的哈希表要考虑到3点要素,哈希函数,冲突解决,装载因子。
哈希函数:
哈希函数要尽量保证分布均匀,不要太耗时,否则对于频繁访问哈希表的场景来说会非常耗时。
冲突解决:
冲突解决上面已经提到过,根据不同的场景使用不同的解决方案。对于链表法的哈希表,我们可以把链表改成红黑树,这样当一个链表很长的情况下也保证了效率。
装载因子:
对于内存很吃紧的情况,我们可以把装载因子设置成大一点,但是在利用开放寻址法的哈希表中,装载因子不能设置成很大,否则发生的冲突概率会很大,效率会极速下降。对于内存消耗不是很敏感的场景,可以设置成小一点,这样哈希表的性能会得到保证。