1、散列表用的是数组支持下标随机访问的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,没有数组就没有散列表
2、得到数组下标的映射方法就叫做散列函数(hash函数),而散列函数计算得到的值就是散列值(hash值)
规律:散列表用的是数组支持下标随机访问的时候,时间复杂度为是O(1)的特性。通过散列函数将元素的键值映射为下标,将数据存在数组中对应下标的位置。当为什么按照键值查询元素时,我们用同样的散列函数,将键值转化为数组下标,从对应的数组下标的位置取数据
1、散列函数计算得到的散列值时一个非负整数
2、如果key1=key2,那么hash(key1)=hash(key2)
3、如果key1≠key2,那么hash(key1)≠hash(key2) 真实情况下,不同的key对应的散列值都不一样,几乎不可能。如MD5.SHA.CRC等哈希算法
解决冲突的方法:开放寻址法和链表法
核心思想:如果出现了冲突,就重新探测一个空闲位置,将其插入。如何从新探测呢?
不管采用哪种探测方法,当散列表空闲位置不多的时候,散列冲突的概率就会大大提高。为了尽可能保证散列表的操作效率,一般情况下,尽可能保证散列表有一定比例的空闲槽。用装载因子来表示空位的多少
装载因子的计算公式是:
散列表的装载因子 = 填入表中的元素个数 / 散列表的长度
装载因子越大,出现 冲突的几率越大,散列表性能下降
插入时,通过散列函数得到对应的散列槽位,将其 插入到对应链表中即可。时间复杂度是O(1)
查找、删除一个元素时,同样通过散列函数计算出对应的槽,然后遍历链表查找或者删除。此时间复杂度和链表的长度k成正比,也就是O(K)。对于散列均匀的散列函数来说,理论上 k = n / m,其中 n 表示散列中的数据个数,m表示散列表中 “槽” 的个数
1、Word文档单词拼写查找功能时如何实现的?
常用单词20w个左右,假设单词平均长度10字母,平均一个单词占用10字节的内存空间,那20w大约占2MB的存储空间,就算放大10倍时20MB.可以完全放在内存里面。所以我们可以用散列表存储整个英文单词词典,当输入一个单词时候,我们就去散列表中去查找。如果查到,说明拼写正确。反之,不正确
2、假设我们有 10 万条 URL 访问日志,如何,按访问次数给URL排序?
遍历10万条数据,以URL为key,访问次数为value, 存入散列表。同时记录访问次数的最大值,时间复杂度为O(n)
如果 K 不是很大,可以使用桶排序,时间复杂度 O(N)。如果 K 非常大(比如大于 10 万),就使用快速排序,复杂度 O(NlogN)
3、有两个字符串数组,每个数组大约有 10 万条字符串,如何快速找出两个数组中相同的字符串?
以第一个字符串数组构建散列表,key 为字符串,value 为出现次数。再遍历第二个字符串数组,以字符串为 key 在散列表中查找,如果 value 大于零,说明存在相同字符串。时间复杂度 O(N)。
1、散列函数不能态复杂,太复杂的函数,势必耗费更多的计算时间,间接影响性能
2、散列函数生成的散列值尽可能的随机,减少 冲突。即使出现冲突,散列到每个槽的数据也会平均,不会出现某个槽内数据特别多的情况
常见的散列函数:
动态扩容
插入一个数,不需要扩容,最好的时间复杂度为O(1)。最坏情况下,需要扩容,需要重新申请内存空间,重新计算hash位置,并且搬移数据,时间复杂度为O(n)。用均摊分析法,均摊的情况下时间复杂度接近最好情况,为O(1)
大部分情况下,插入一个数据都很快,但是当装载因子达到阈值时,需要进行扩容,这时候插入一个数据时会变的很慢,当数据足够多的时候就难以接受
为了解决一次性扩容耗时过多的情况,我们可以将扩容操作穿插在插入操作的过程中,分批完成。当装载因子触达阈值之后,我们只申请新空间,但并不将老的数据搬移到新散列表中。将一次性扩宽的代价均摊到每次插入的操作 中,这种情况下的时间复杂度为O(1)
1、开放寻址法:
优点:借助CPU缓存机制(局部性原理)加快查找速度,并且序列化简单
缺点:删除数据的时候麻烦,需要特殊标记已经删除的数据,所有数据都存在一个数组,冲突的代价更高
总结:数据量比较小,装载因子小的时候,适合采用开放寻址法。这也是Java中的ThreadLocalMap使用的开放寻址法解决冲突的原因
2、链表法:
总结一下,基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表。
JAVA中使用散列表的数据类型:
HashTable:
1、默认初始大小:11
2、装载因子:0.75
3、散列函数:int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
4、当装载因子大于0.75时,启动扩容机制
4、冲突解决方法:使用单链表解决hash冲突
HashMap:
1、默认初始大小:16
2、装载因子:0.75
3、散列函数:
hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
4、当装载因子大于0.75时,启动扩容机制
5、使用单链表解决hash冲突,当链表长度大于8,将单链表转换成红黑树
ThreadLocalMap
1、初始容量:16
2、装载因子:2/3
3、散列函数:
hash(Object key) {
int HASH_INCREMENT = 0x61c88647;
AtomicInteger nextHashCode = new AtomicInteger();
nextHashCode.getAndAdd(HASH_INCREMENT)
int threadLocalHashCode = nextHashCode()
int i = threadLocalHashCode & (table.length - 1);
}
4、当装载因子大于2/3时,启动扩容机制
5、使用线性探测的开放地址法解决hash冲突
首先思考,何为一个工业级达到散列函数,具有哪些特性?
1、支持快速查找,插入,删除操作
2、内存占用合理,不能浪费过多的内存空间
3、性能稳定,极端情况下,散列表的性能也不会退化到无法接受的情况
如何实现一个散列表?
1、选择合适的散列函数
2、定义适中的装载因子阈值,并设计动态扩容策略
3、选择合适的散列 冲突解决方法
https://www.jianshu.com/p/64f6de3ffcc1