随着网络和大数据时代的到来,我们如何从海量的数据中找到我们需要的数据就成为计算机技术中不可获取的一门技术,特别是近年来抖音,快手等热门短视频的兴起,我们如何设计算法来从大量的视频中获取当前最热门的视频信息呢,这就是我们今天即将谈到的Hash和布隆过滤器。以下是Hash和布隆过滤器的一些常见应用:
假如我们需要从海量数据中查询某个字符串是否存在?如果让你设计一种数据结构,你会想到哪些数据结构呢?链表和数组(直接排除,查询复杂度为0(n)),二叉树(红黑树,AVL树,时间复杂度o(log(n)),可以考虑),平衡多叉树(B树,B+树等,时间复杂度为h.log(n),其中h为树的层高),Hash(时间复杂度为O(1)),下面分别介绍这些数据结构
增删改查时间复杂度为O(logn) :比如100万个节点,最多比较 20 次;10 亿个节点,最多比较 30 次;
平衡的目的时保证二叉树的左右节点的高度都差不多,这样二叉树才能保证时间复杂度为O(logn),否则最坏的情况,二叉树的时间复杂度为O(n),退化为线性表,插入数据的时候时按顺序插入的。
平衡二叉树是中序遍历有序(左子树的key<根节点的key<右子树的key),每次比较都能保证到左子树或者右子树,每次都能排除一半的元素达到快速索引的目的.元素的比较是使用的二分查找(每次搜索都能排除一半),使用到二分查找的结构如下图所示:
有序数组和平衡二叉搜索树使用二分查找无可厚非,对于B树和B+树而言,其实跟平衡二叉搜索树类似,只是B树的一个节点有多个KEY,每个节点有多个孩子,每个节点内的KEY都是有序的。因此在查找KEY位于哪个节点时,也使用到了二分查找,关于跳表的数据结构请参考其他博客。
如果对平衡二叉树和平衡多叉树有兴趣的同学,可以参考我的博客
1.B树和B+树的分析和实现
2.红黑树的分析与实现
前面提到的平衡二叉树的时间复杂度为O(log(n)),效率还是蛮高的,不过对于像字符串作为的key时而进行比较时,还是比较耗时的,因此有没有一种更高效的算法来完成字符串的比较呢,那就是使用散列表来完成,散列表是使用hash函数将一个key映射到一个数据表中,这样在没有冲突的情况下,根本不需要字符串的比较,只要在查询的时候,如果相应的key映射的下标中存在元素,即可完成数据的查询。
散列表是根据key计算key在表中的位置的数据结构,是 key 和其所在存储地址的映射关系;在插入数据时,需要将散列表的节点中的key和value一起存储到表中。 为什么需要存储key,这时因为在查询时,需要将查询的key和表中的key进行比较,看是否相等,如果不等,则代表这次查询失败(不等,则代表存在哈希冲突,表中的这个位置被其他key所拥有)
映射函数 Hash(key)=addr ;hash 函数可能会把两个或两个以上的不同 key 映射到同一地址,这种情况称之为冲突(或者 hash 碰撞);由于存在冲突情况,因此在选择hash函数时,需要满足以下2个条件,这样才能保证冲突的概率最小化和查询效率。
通常常用的哈希函数有:murmurhash1,murmurhash2,murmurhash3,siphash(redis6.0当中使⽤,rust等大多数语言选用的hash算法来实现hashmap),cityhash 都具备强随机分布性;测试地址如下:https://github.com/aappleby/smhasher,siphash主要 解决了字符串接近的强随机分布性,作为redis的hash算法时因为,在redis中,经常使用uid:1000和uid:1001这样的key,这2个key很接近,如果使用其他的算法,很可能都映射到同一个地址,而siphash却可以让这2个key映射到不同的地址。
数组存储元素的个数 / 数据长度;用来形容散列表的存储密度;负载因子越小,冲突越小,负载因子越大,冲突越大;
不管如何优秀的hash算法,都不可避免的让不同的key映射到同一个地址,那么如何处理这样的情况,即如何处理hash冲突呢。主要有链表法和开发寻址法
引用链表来处理哈希冲突;也就是将冲突元素用链表链接起来;这也是常用的处理冲突的⽅
式;但是可能出现一种极端情况,冲突元素比较多,该冲突链表过长,这个时候可以将这个
链表转换为红黑树;由原来链表时间复杂度 转换为红黑树时间复杂度 ;那么判断该链表过长的依据是多少?可以采⽤超过 256(经验值)个节点的时候将链表结构转换为红黑树结构;
将所有的元素都存放在哈希表的数组中,不使用额外的数据结构;一般使用线性探查的思路
解决;
Hk(key) = [GetHash(key) + k * (1 + (((GetHash(key) >> 5)
+ 1) %(hashsize – 1)))] % hashsize
在此 (1 + (((GetHash(key) >> 5) + 1) % (hashsize – 1))) 与 hashsize互为素数(两数互为素数表示两者没有共同的质因⼦);执⾏了 hashsize 次探查后,哈希表中的每⼀个位置都有且只有⼀次被访问到,也就是说,对于给定的 key,对哈希表中的同⼀位置不会同时使⽤ Hi 和 Hj;
既然hash的查询效率已经达到了O(1),效率已经达到了常数,那么我们需要从海量数据中(比如10亿条)查询某个字符串是否存在时是否可以使用hash来完成查询呢,其实是不可以的,虽然hash查询效率很高,也不需要比较字符串,但是需要将字符串存储到内存中,那么多条数据的字符串key存储到内存中是不现实的,那么是否有不需要字符串key的数据结构就能知道相应的元素是否存在呢?布隆过滤器的出现就是解决这个问题的。
布隆过滤器是一种概率型数据结构,它的特点是高效地插入和查询,能确定某个字符串一定不存在或者可能存在(即存在一定的误差,本来字符串key不存在,却被视为字符串存在);
布隆过滤器不存储具体数据,所以占用空间小,查询结果存在误差,但是误差可控,同时不支持删除操作;
在说明布隆过滤器如何实现之前,先来了解一下,如何采用一个算法将一个字符串key映射到位图中的某一位中。比如我们有一个8 * 8 的位图,另有一个字符串val(假如为"thestringkey"),采用上面或者任意的一种hash算法,比如得到 hash(val) = 173,那么如何将hash值173映射到
下面的二维位图中呢
了解玩位图操作之后,就很容易理解布隆过滤器原理了。
当一个元素加入位图时,通过 k 个 hash 函数将这个元素映射到位图的 k 个点,并把它们置为 1;当检索时,再通过 k 个 hash 函数运算检测位图的 k 个点是否都为 1;如果有不为 1 的点,那么认为该 key 不存在;如果全部为 1,则可能存在**(这些1可能是由其他key映射的,这也是布隆过滤器存在误差的原因,什么时候使用布隆过滤器呢,在应用场景里面以缓存穿透来进行说明)**;
为什么不支持删除操作?
在位图中每个槽位只有两种状态(0 或者 1),一个槽位被设置为 1 状态,但不确定它被设
置了多少次;也就是不知道被多少个 key 哈希映射而来以及是被具体哪个 hash 函数映射而
来;
后续在完成