背景
-
使用 word 文档时, word 如何判断某个单词是否拼写正确?
-
垃圾邮件过滤算法如何设计?
-
网络爬虫程序,怎么让它不去爬相同的 url 页面?
以上都是基于某些字符串为目标,通过hash在海量的数据中查询筛选出的应用背景
常用的高效查找--二分查找
原理:前提先是保证数据的有序性,再通过比较每次排除一半的元素达到快速索引的目的
采用二分查找的数据结构:
- 有序的数组
- 平衡二叉树( AVL树 和 红黑树)
- 平衡多路搜索树(B-tree & B+tree)
- 多层级有序链表(跳表)
时间复杂度:O( ),意味着当在100万个数据中查找,最多比较20次就能找到目标,10亿个数据最多比较30次便可找到目标
散列表
散列技术与二分查找法相比,最大的不同是它摒弃了“关键码有序”的先决条件,在海量的数据中若通过二分查找比较字符串来查询数据会大大降低查找的效率,而散列表则采用的是 基于key通过一个hash函数计算出key在表中的位置地址,是key和其所在存储地址的映射关系,从而大大提升搜索效率
散列表的组成
-
hash函数
-
数组
通过hash函数计算key在数组中的地址,再将kv节点存放在这个地址中,由此组成整个数组
hash的选择条件
-
计算速度快
-
强随机分布(等概率、均匀地分布在整个地址空间)
开发中的hash函数选择
-
murmurhash1(计算速度最快,但质量一般)
-
murmurhash2(计算速度快,质量也有保证,其使用最为广泛)
- murmurhash3(计算速度比较慢,但质量是最好的)
- siphash 主要解决字符串接近的强随机分布性( redis6.0 当中使用,rust 等大多数语言选用的 hash 算法来实现 hashmap)
-
cityhash(同样具有强随机分布性,应用也比较广泛)
作用:更快更好地实现key与存储地址的映射关系
散列表的操作流程
- 插入
- 搜索
无论插入还是搜索,都是一个key值通过hash函数的多次运算得到相同的值,这个值对不变的数组长度进行取余操作,得到数组中对应的槽位地址,从而定位到节点的存储位置,实现搜索和插入
hash冲突及其解决
- 冲突
若两个或以上的key通过hash函数的计算,可能被映射到数组的同一个地址,从而造成hash冲突;
- 负载因子 (描述冲突的激烈程度)
冲突的解决
前提:负载因子在合理范围内,即 数组存储元素的个数 < 数组的长度
- 链表法(拉链法):将冲突的元素通过链表的连接方式存放在同一数组槽位地址中,这也是冲用的处理冲突的方法,但当冲突元素过多使链表的长度过大,从而造成极端情况,这个时候可以将这个链表转换为红黑树、最小堆,由原来链表时间复杂度转换为红黑树时间复杂度;那判断该链表过长的依据是多少?可以采用超过 256(经验值)个节点的时候将链表结构转换为红黑树或堆结构;(拉链法的数组是指针数组,每个槽位都是指针)
- 开放寻址法:同构算法将所有的元素都存放在哈希表的数组中,不使用额外的数据结构;一般使用线性探查的思路解决;
步骤:
1、当插入新元素的时,使用哈希函数在哈希表中定位元素位置;
2、检查数组中该槽位索引是否存在元素。如果该槽位为空,则插入,否则3;
3、在 2 检测的槽位索引上加一定步长接着检查2; 加一定步长分为以下几种:
· i+1,i+2,i+3,i+4, ... ,i+n
· i-1² ,i+2² ,i-3² ,1+4² , ...
这两种都会导致同类 hash 聚 集;也就是近似值它的hash值也近似,那么它的数组槽位也靠近,形成 hash 聚集;第一种同类聚集冲突在前,第二种只是将聚集冲突延后;
另外还可以使用双重哈希 来解决上面出现hash聚集现象:
当负载因子不在合理范围内时,即 实际的存储元素 < 数组的 0.1size 或 实际的存储元素 > 数组实际的存储元素 ,要想解决冲突要对数组进行 缩容 或 扩容 ,缩容只是为了避免存储空间的浪费,而扩容才是真正的解决中途,而后还要进行rehash(重新哈希) ,因为扩容是翻倍扩容,算法公式:hash(key) % size = Index 中的 size 已经发生改变,所以要重新计算
STL unordered_* 散列表实现
在 STL 中 unordered_map、unordered_set、 unordered_multimap、unordered_multiset 四兄弟底层实 现都是散列表;
原理图:采用拉链法解决冲突,优化:为了实现迭代器,将数组中的各个链表或节点串成一个单链表
布隆过滤器
背景
当要确认一个key是否在文件中,可以将整个文件加载到内存中,然后再进行查询判断,但由于内存有限,一般不选择这种方式,且我们只需要知道key是否存在,而不用获取对组中的value内容,所以选择先将每个对组元素中的key标记到布隆过滤器中,然后再将每个 keyvalue对 存入文件中,最后便可直接通过布隆过滤器判断出key是否在文件中了
构成
位图(bitmap 每一槽位只存放0或1的数组)+ n个hash函数
通过hash函数和一些公式计算元素的存储位置,举例:
确定索引位置的操作:hash(key) %bit_size = Index
原理
为什么不支持删除操作
![](https://i-blog.csdnimg.cn/blog_migrate/773ab0f6ba0e924deb9b880af4685dda.png)
如上图:在所圈的槽位中因被映射而置为了1,但却无法确定它是由哪个hash函数映射而来,也无法确认有多少个str的key通过hash映射到该槽位,若因为不需要某个key而将该槽位的状态改为0,则会影响对其他key是否存在的判断
应用场景
![](https://i-blog.csdnimg.cn/blog_migrate/98fafe840a27899571c04a4ccf8399bb.png)
布隆过滤器的参数和计算公式
根据 n 和 p,算出 m 和 k
总结
- 布隆过滤器是一种概率型数据结构,它的特点是高效地插入和 查询,能确定某个字符串一定不存在或者可能存在;
- 布隆过滤器不存储具体数据,所以占用空间小,查询结果存在误差,但是误差可控,同时不支持删除操作;
分布式一致性hash
背景
随着存储数据的增加,我们需要将数据存储到不同的节点的kv中,随之就需要对缓存进行扩容,就是增加存储的节点,但存储 kv对 的地址会被公式 hash(key) % size 映射出对应的索引值Index并存储在缓存当中,若要扩容就会改变size的值 从而改变原来 kv对 存储地址映射在缓存的索引值,造成无法通过缓存中的索引值找到 kv对 真正的存储地址,即造成缓存失效
解决:可以固定算法,即将算法中的变量size固定下来,将size固定为 2^32 ,但我们真的要设置一个 2^32 大小的数组来进行存储吗?显然不现实,所以还要改变查找节点映射关系,即改变计算映射的算法,分布式一致性hash便采用了此方法
算法原理
![](https://i-blog.csdnimg.cn/blog_migrate/07197c0f3c2b8185e6b98e477d658340.png)
应用场景
- 分布式缓存;将数据均衡地分散在不同的服务器当中,用来分 摊缓存服务器的压力;
- 解决缓存服务器数量变化尽量不影响缓存失效;
仍存在的问题及以解决
问题1
如上图,当在 .102 和 .100 的服务器地址区间插入新的服务区地址 .103 时,根据顺时针规定 会造成在原本在 .102 和 .100 区间 存放在.100地址的节点 的映射索引有部分变成 .103地址的映射索引,但我们在.103地址寻找是查询不到原来的节点,像上图就是会无法在 .103地址中找到k3节点数据,因此造成部分的缓存失效。
解决方法:hash迁移
仍如上图,将在 .102 到 .103区间原本存储在 .100 地址的节点数据全部迁移到 .103 中,但仍没有完全解决局部缓存失效的问题;
问题2
hash偏移:hash 算法得到的结果是随机的,不能保证服务器节点均匀分布在哈希环上;分布不均匀造成请求访问不均匀,服务器承受的压力不均匀; 本质是存储的样本数太少造成的,因为样本数越大hash算法的才越稳定,节点的分布才会更均匀分布在哈希环地址中;
解决方法:引入虚拟节点
![](https://i-blog.csdnimg.cn/blog_migrate/128b215a79ed935f0060f368fc61f902.png)
在一个服务器端口地址后面增加 : 6000 : "1~255 中的编号",则带有 .100编号地址的节点就会随机均匀地分布在哈希环上 ,其他的端口地址同理也可引入多个虚拟节点。
推荐一个零声学院免费公开课程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:零声教育