版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012292754/article/details/84475996
1 散列介绍
散列表,又称 Hash Table
,哈希表。用的是数组支持按照下标随机访问数据的特性,所以散列表是数组的扩展,由数组演化而来。
1.1 设计散列函数的要求
- 由散列函数得到的散列值是非负整数;
- 如果 key1 = key2, 那么 hash(key1) = hash(key2);
- 如果 key1 不等于 key2, 那么 hash(key1) 不等于 hash(key2);
1.2 解决散列冲突的方法
在真实的情况下,找到一个不同key对应的散列值不一样的散列函数,几乎不可能。
1.2.1 线性探测(Linear Probing)
往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用,就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。
1.2.2 二次探测(Quadratic Probing)
它和线性探测很像,线性探测每次探测步长1,即hash(key)+0,hash(key)+1…;
二次探测就是探测的步长变成平方,即hash(key)+0,hash(key)+1^2…;
1.2.3 双重散列(Double hashing )
不仅要使用一个散列函数,使用的是一组散列函数,hash1(key),hash2(key)…,先用第一个散列函数,如果计算得到的存储位置已经被占用,再用第二个散列函数,直到找到空闲的位置。
1.3 装载因子(load factor)
装载因子 = 填入表中的元素个数 / 散列表的长度
装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。
1.4 链表法
在散列表中,每个桶(bucket) 或者 槽 (slot) 会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中。
1.5 思考题
- 0 万条 URL 访问日志,如何按照访问次数给 URL 排序
遍历10万条数据,用URL 为 key,访问次数作为 value,存入散列表,同时记录下访问的最大次数MAX, 时间复杂度 O(n);
如果MAX不是很大,用桶排序,时间复杂度 O(n)。如果MAX 非常大,就用快排,时间复杂度 O(n logn)
- 有两个字符串数组,每个数组大约有 10 万条字符串,如何快速找出两个数组中相同的字符串?
用第一个字符串数组构建散列表,key 为字符串,value 为出现次数。然后再次遍历第二个字符串数组,以字符串为key 在散列表中查找,如果value > 0,说明存在相同的字符串,时间复杂度O(n)
2 如何设计散列函数
散列函数设计的好坏,决定了散列表冲突的概率大小,也直接决定了散列表的性能。
- 散列函数不能设计太复杂,如果过于复杂,势必会消耗很多计算时间,;
- 散列函数生成的值尽可能随机且均匀分布;
2.1 散列函数的设计方法
直接寻址法,平方取中法,折叠法,随机数法,
2.2 散列冲突
Java 中 LinkedHashMap 就采用了链表法解决,ThreadLocalMap通过线性探测的开放寻址法来解决。
2.2.1 开放寻址法
- 优点 : 散列表的数据存储在数组中,有效利用CPU缓存加快查询速度;序列化比较简单,而链表包含指针,序列化比较困难;
- 缺点:删除数据比较麻烦,需要特殊标记已经删除的数据;所有数据存储在数组中,比起链表,冲突的代价更高;装载因子不能太大,比链表法更加浪费内存
- 当数据量小,装载因子小的时候,适合采用开放寻址法;
2.2.2 链表法
- 优点:对内存的利用率比开放寻址法高,因为链表节点可以在需要的时候再创建,不需要开放寻址法那样先申请好;
- 对大装载因子的容忍度更高,开放寻址法只适用装载因子小于1,接近1时就会有大量的散列冲突。但是对于链表,只要散列函数的值随机均匀,即使装载因子变成10,也就是链表长度变长,查找效率有所下降
- 适合存储大对象、大数据量的散列表,比起开放寻址法,它更加灵活,支持更多的优化策略,如可以用红黑树代替链表;
3 HashMap 举例
- 初始大小
HashMap默认初始大小是16,这个初始值可以设置 - 装载因子和动态扩容
最大的装载因子默认0.75,当HashMap元素超过 0.75*capacity,就会启动扩容,扩容为原来的2倍。 - 散列冲突的解决
HashMap 底层采用链表法解决散列冲突。在JDK1.8中,进行了优化,引入了红黑树。当链表长度太长(默认8),链表就转换为红黑树。利用红黑树的快速增删改查的特点提高性能。当红黑树节点个数少于8,红黑树又转换为链表。因为在数据量少的时候,红黑树要维护平衡,比起链表,性能优势不明显。
4 设计一个工业级的散列表
要求:
- 支持快速插入、删除、查询操作;
- 内存占用合理;
- 性能稳定,在极端情况散列表的性能也不会退化到无法接受;
4.1 如何实现一个工业级的散列表
- 设计一个合适的散列函数;
- 定义装载因子的阈值,设计动态扩容的策略;
- 选择合适的散列冲突算法