1. 布隆过滤器
1.1简介
布隆过滤器实际上是一种数据结构,它可以实现在某些特定场景下的高效的查找。这种高效体现在,**在很低的存储空间上进行快速的定位。**我们拿黑名单举例,在搜索引擎中,想定位某个网站是否是黑名单,把待查询的URL输入布隆过滤器中,此时如果是黑名单,那么一定可以查出来。
布隆过滤器存在一定的失误率,即一个输入不在过滤器中,我也有可能判断其在过滤器中。拿黑名单举例,如果一个网站是黑名单,那么它一定会被过滤,如果不是,它也有可能会被过滤。这和布隆过滤器的原理是有关系的。
布隆过滤器需要对hash函数有个认识。hash函数可以将输入的任意多数字、字符得到一个hashcode
,长度为是一个固定的值,通常是16的整数倍。那么问题来了,一个函数的输入是不限定的,输出是一个确定的个数,所有一定会存在多个输入对应一个输出,这是显而易见的,这在hash函数中有个高大上的叫法叫碰撞,也很形象。说到这,我们大概理解了:同一个数据输入hash函数一定可以得到相同的输出,不同的数据输入hash函数可能会得到相同的输出,这种现象称为碰撞。
下图简单描述了hash函数与HashMap:
Hash函数可以说是大名鼎鼎了,hash函数的特点如下:
-
单向性:在hash函数中,无法从输出结果推导出输入结果,保证了安全性,比如MD5码(一种摘要算法)和SHA。
-
碰撞约束: 不能同时找到两个不同的输入使其输出结果一致。
为什么在计算机中使用hash函数?
-
进过Hash函数的处理的数据会均匀的分布在一个区域内,比如一个数据集中许多数据集中出现在某些部分,但是在别的部分很稀疏,这些数据经过hash函数处理就可以几乎以一个平均的状态分布在整个数据集上,前提是数据集相当大。这一点是hash函数的设计导致的,这涉及到严谨的数学证明,这里不讨论。
-
输出的结果是确定的。我们可以通过查找hashcode来确定一个数据是否存在于集合中,布隆过滤器就是利用这一点。
-
计算机中可以做到对一个数据进行hash处理的时间复杂度为O(1),java中为O(logN),开销很小。
1.2 原理
说了这么多,布隆过滤器到底是如何工作的呢?首先布隆过滤器中的存储结构在逻辑上来说为位数组,常用基础数据类型来实现这个数组。为了方便理解,这里举例数据为字符串。
操作过程:
输入的字符串会经过K个hash函数处理(这些hash函数必须是无关的,这并不难得到),得到了K个结果,把这些结果对arr
进行hashMap
操作,就会得到在数组中的K个位置,把这K个位置标位1,当输入了许多字符后,数组中有许多部分已经被标志位1了。
当新来一个字符串时,继续对其进行重复的操作,观察这K个位置是否有不为1的位置,如果有则不被过滤,否则被过滤。
废话不多说,上图:
下面说几个细节:
- 首先,数组长度必须随着输入样本量的而调整,当输入量很大,而数组不是很大,会导致整个数组几乎所有位置都是1,无论什么字符串输入都会被过滤。
- 过滤器存在失误率。这和数组开辟的长度和样本量有关系。即我新输入的字符串的K个位置都被其他字符串给标记过了,那么我只会把这个字符串过滤掉。
相关公式:
M
=
−
n
∗
l
n
P
(
l
n
2
)
2
M = - \frac{n * lnP}{(ln2)^2}
M=−(ln2)2n∗lnP
其中n为样本的数量,p为可以接受的失误率,M为开辟的数组长度。
K
=
l
n
2
∗
m
n
,
其
中
K
为
h
a
s
h
函
数
的
个
数
K = ln2 * \frac{m}{n},其中K为hash函数的个数
K=ln2∗nm,其中K为hash函数的个数
在实际的计算中,我们会选择最优的情况,即我们计算出来M和K大概率不是整数,我们此时向上取整,即我们的数组多开辟了一些空间,哈希函数也比理论上多一点,那么我们在查找过程中,出现失误的概率就会减少(想想导致失误的原因)。真实的失误率为:
P
=
(
1
−
e
−
n
∗
k
M
)
K
,
p
为
失
误
率
P = (1 - e ^{\frac{-n * k}{M}})^K ,p为失误率
P=(1−eM−n∗k)K,p为失误率
1.3 布隆过滤器的优势与应用
当我们需要处理几亿条数据时,如果采用hashmap来存储这个记录,容量巨大,而采用布隆过滤器则会大大降低存储的空间。
相对于简单的数据结构存储来说,布隆过滤器的通过使用多个hash函数使得错误率变得很低。
布隆过滤器可以应用在:
- 黑名单判断
- 网络爬虫判断网站是否被处理过了
- 垃圾邮件过滤等
1.3代码实现
public class BloomFilter {
public static int NUM = 1024;
private static int[] arr = new int[NUM]; //可以表示1024 * 32个数据
//这里的code是已经对M取过模的数字了,M为逻辑上的位数组的长度
public static void setFlag(int code)
{
//定位其所在的整数下标
int index = code / 32;
//定位其坐在整数的字节下标
int bitIndex = index % 32;
arr[index] = (arr[index] | (1 << bitIndex));
}
}
参考:
- 左程云算法课
- https://www.jianshu.com/p/88c6ac4b38c8