什么是散列表?
散列表也被称“Hash Table”,我们称之为“哈希表”或者“Hash表”。散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,没有数组,就没有散列表。
最简单的散列表
最简单的散列表就是key对应数组的下标,value就是数组下标内的元素值。
我们把key到value的这个过程赋予一个函数,称为散列函数。散列函数有3个基本要求:
1.散列函数计算的散列值是一个非负整数
2.如果key1 = key2,那hash(key1) == hash(key2)
3.如果key1 != key2,那hash(key1) != hash(key2) ------------- 散列冲突
散列函数的设计尽量使的哈希值随机且均匀分布
散列冲突
第三点好理解,但是难实现。目前业界著名的MD5,SHA,CRC等哈希算法也完全无法避免这种哈希冲突。而且,因为数组的存储空间有限,也会加大散列冲突的概率。
既然几乎无法找到一个完美的无冲突的散列函数,即使可以,也需要花费很多的人力物力,那么我们针对散列冲突问题,就需要其他途径来解决。
开放寻地址法
1.线性探测法
简单来说就是依次找下去
插入:如果发生了散列冲突,就往后找一个空位子,然后插进去,如果后面没了,就从头开始
查询:如果发生了散列冲突,就接着找,直到找到元素(找到)或者找到空的(没找到)
删除:删除要注意的点就是,得标记删除后的格子为deleted,不然在查询的时候会出现问题
可以看得出来,这个方法很低效
一般有一个装载因子,装载因子是数组最大可容纳的数据所占的比例。装载因子越大,数组使用率越高,冲突几率越高。javaHashMap一般设定为0.75
2.二次探测法
线性探测法是hash(key) + 1,hash(key) + 2 ~~~~~
二次探测法就是hash(key) + 1^2,hash(key) +2^2~~~~~
3.双重散列法
双重散列法的意思就是,我们不是使用一个散列函数,而是使用一组散列函数,当hash1(key)被占用的时候,就是用hash2(key),以此类推到hashn(key),直至找到空的格子
链表法
这种方法很好理解,就是我接受你的散列冲突,然后在格子里面加个链表。这样的话就算冲突也没事了,删除的话也更好。
散列碰撞
以链表法为例,hash(key)为同一个值,然后把数据都塞到同一个槽里面,查询的时间复杂度就是O(1)退化成O(n)。这样就可能因为查询操作消耗大量CPU或线程资源,导致系统无法响应其他请求,从而达到拒绝服务攻击(Dos)的目的。这也就是散列表碰撞攻击的基本原理。
如何避免低效的扩容
这只针对于以数组为底层数据结构的散列表。
采用的方式是当使用比例超过装载因子的时候申请一个新的资源,然后新数据和老数据同时插入,老数据一次只插一个,这样子的话,动态扩容过程无感。不会像之前一样整体copy之后再插入。
工业级散列表举例分析
1.初始大小
HashMap默认值为16,如果提前知道数据集大小可以提高效率
2.装载因子和动态扩容
最大装载因子默认为0.75
当HashMap中元素个数超过0.75*capacity的时候,就会启动扩容,每次为2倍
3.散列冲突解决方法
在JDK1.8中,当链表长度太长(默认超过8时),链表就转为红黑树,反之亦然。
4.散列函数
设计规则简单高效,随即均匀