这应该是看完最呆(没有想到的那种呆~)的一个小章节了,给作者鼓掌,讲的好好。果然抽象能力才是王道
文章目录
1、散列表
核心:散列表用的是数组支持按照下标随机访问数据的特性。
这个例子举的好好~不抄了,粘原文,重点是下面的亮条条(广告
跟着学了几个排序算法后,此时此刻看到散列表想到的是计数排序呢,因为都是想着法儿地给元素和数组下标搞关系。
1.1、小概念
- 键:也叫关键字,就是最终放到数据结构中的元素啦
- 散列函数:也叫哈希函数。算命先生,告诉键应该去数组的哪个坑里蹲着
- 散列值:也叫哈希值。键蹲的那个坑
1.2、散列函数
如果把元素都对应到了数组中,查找的时间复杂度就是O(1),看着很酷呢~
散列函数需要满足三点基本要求:
- 散列函数计算得到的散列值是一个非负整数
- 如果key1 = key2,那么hash(key1) = hash(key2)
- 如果key1 != key2,那么hash(key1) != hash(key2)
(补充一条:简单不烧脑更好)
1.3、散列冲突
实际中,比较难满足第三点要求,当存在key1 != key2,hash(key1) != hash(key2)时称作散列冲突。解决散列冲突常见的两种办法:
- 开放寻址法
思路:出现冲突就重新探测空闲可用的位置来存储数据。一种简单的线性探测方法如下。
删除操作:因为使用开放寻址法的时候,key相同的数据存储在同一个位置(但是我们不晓得是有几个相同的数据存储在这一份数据中),所以删除的时候不能直接删,而是标记位deleted,避免被寻址覆盖了。(会有很多的空间浪费吧~不环保,差评!)
【性能分析】因为算完还需要找合适的位置,最坏的情况可能需要挨个儿找一遍,O(n)啦
【更好的办法】
二次探测:探测步长变成n^2。
双重散列:使用一组散列函数,挨个算,知道有一个函数算出来没被占用的位置为止(这组散列函数应该很不容易吧)
- 链表法
思路:将散列值相同的元素用链表存起来,数组里存储的是这个链表的头的信息。
1.4、装载因子
装载因子可以用来表示数组中空位的多少,装在因子越大,说明空闲位置越少,冲突越多,散列表性能会下降。
load factor = 填入表中的元素的个数 / 散列表长度
2、实际应用中的散列表注意事项
2.1、散列函数的设计原则
- 不能太复杂,避免消耗太多的计算时间
- 生成的散列值要尽可能随机并且均匀分布
2.2、装载因子过大
动态扩容。
散列表的扩容需要重新计算哈希位置,搬移数据。装载因子特别小时,如果对空间消耗敏感,还可以动态缩容。
2.3、如何避免低效地扩容
避免一次性扩容,将新数据插入新的散列表的过程中搬移旧数据到新的散列表
2.4、解决冲突的方案选择
【开放寻址法】
- 优点
数据存储在数组中,可以利用CPU缓存加快查询速度。
序列化相对简单 - 缺点
删除数据比较麻烦,更浪费内存空间。 - 适用场景
数据量比较小,装载因子小
【链表法】
- 优点
内存利用率高
对大装载因子的容忍度更高 - 缺点
消耗内存
非连续存储,对CPU缓存不友好。可以通过使用其他数据结构来替代链表优化效率
- 适用场景
存储大对象、大数据量的散列表
2.5、设计一个工业级的散列函数
像这种要考虑很多方面的问题,大致都只能给个方针啥的,作者给的,我抄过来啦~遇到的时候还能回来翻翻看:
一个工业级的散列表需要满足以下要求:
- 支持快速的查询、插入、删除操作
- 内存占用合理,不能浪费过多的内存空间
- 性能稳定,极端情况下,散列表的性能也不会退化到无法接受的情况
如何实现:
- 设计一个合适的散列函数
- 定义装载因子阈值,并且设计动态扩容策略
- 选择合适的散列冲突解决办法
举栗子的时候给了个HashMap的散列值计算的方法,看完头皮发麻。评论区有小牛角给了分析呢,好好看完,就抄过来啦
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
找了一下位运算技巧:面试常用位运算技巧
位运算的这些没有找到可以总结的办法,所以记不住啦~回头找机会再瞄一瞄吧。
**TODO:**LRU实现
3、哈希算法
原理:将任意长度的二进制值串映射为固定长度的二进制值串的规则。
一个优秀的哈希算法要满足的几点要求:
- 从哈希值不能反向推导处原始数据
- 对输入数据非常敏感,哪怕原始数据只修改了一个Bit,哈希值也大不相同
- 散列冲突概率很小
- 执行效率高
应用
- 安全加密
- 唯一标识
- 数据校验
- 散列函数
- 负载均衡
- 数据分片
- 分布式存储
一致性哈希