大数据算法——布隆过滤器

本文始发于我的公众号:TechFlow,原创不易,求个关注python


今天的文章和你们一块儿来学习大数据领域一个常常用到的算法——布隆过滤器。若是看过《数学之美》的同窗对它应该并不陌生,它常常用在集合的判断上,在海量数据的场景当中用来快速地判断某个元素在不在一个庞大的集合当中。它的原理不难,可是设计很是巧妙,老实讲在看《数学之美》以前,我也没有据说过这个数据结构,因此这篇文章也是我本身学习的笔记。算法


原理


在我以前的理解当中,若是想要判断某个元素在不在集合当中,经典的结构应该是平衡树和hash table。可是不管是哪种方法,都逃不开一点,都须要存储原值。数组

好比在爬虫场景当中,咱们须要记录下以前爬过的网站。咱们要将以前的网址所有都存储在容器里,而后在遇到新网站的时候去判断是否已经爬过了。在这个问题当中,咱们并不关心以前爬过的网站有哪些,咱们只关心如今的网站有没有在以前出现过。也就是说以前出现过什么不重要,如今的有没有出现过才重要。数据结构

咱们利用平衡树或者是Trie或者是AC自动机等数据结构和算法能够实现高效的查找,可是都离不开存储下全部的字符串。想象一下,一个网址大概上百个字符,大约0.1KB,若是是一亿个网址,就须要10GB了,若是是一百亿一千亿呢?显然这么大的规模就很麻烦了,今天要介绍的布隆过滤器就能够解决这个问题,并且不须要存储下原值,这是一个很是巧妙的作法,让咱们一块儿来看下它的原理。app

布隆过滤器自己的结构很是简单,就是一个一维的bool型的数组,也就是说每一位只有0或者1,是一个bit,这个数组的长度是m。对于每一个新增的项,咱们使用K种不一样的hash算法对它计算hash值。因此咱们能够获得K个hash值,咱们用hash值对m取模,假设是x。刚开始的时候数组内所有都是0,咱们把全部x对应的位置标记为1。数据结构和算法

举个例子,假设咱们一开始m是10,K是3。咱们遇到第一个插入的值是”线性代数“,咱们对它hash以后获得1,3,5,那么咱们将对应的位置标记成1.学习

而后咱们又遇到了一个值是”高等数学“,hash以后获得1,8,9,咱们仍是将对应位置赋值成1,会发现1这个位置对应的值已是1了,咱们忽略就好。大数据

若是这个时候咱们想要判断”几率统计”有没有出现过,怎么办?很简单,咱们对“几率统计”再计算hash值。假设获得1,4,5,咱们去遍历一下对应的位置,发现4这个位置是0,说明以前没有添加过“几率统计”,显然“几率统计”没有出现过。网站

可是若是“几率统计”hash以后的结果是1,3,8呢?咱们判断它出现过就错了,答案很简单,由于虽然1,3,8这个hash组合以前没有出现过,可是对应的位置都在其余元素中出现过了,这样就出现偏差了。因此咱们能够知道,布隆过滤器对于不存在的判断是准确的,可是对于存在的判断是有可能有错误的。spa


代码


布隆过滤器的原理很简单,明白了以后,咱们很容易写出代码:

# 插入元素
def BloomFilter(filter, value, hash_functions):
    m = len(filter)
    for func in hash_functions:
        idx = func(value) % m
        filter[idx] = True
    return filter
    
# 判断元素
def MemberInFilter(filter, value, hash_functions):
    m = len(filter)
    for func in hash_functions:
        idx = func(value) % m
        if not filter[idx]:
            return False
    return True


错误率计算


以前的例子当中应该展现得很明白了,布隆过滤器虽然好用,可是会存在bad case,也就是判断错误的状况。那么,这种错误判断发生的几率有多大呢?

这个几率的计算也不难:因为数组长度是\(m\),因此插入一个bit它被置为1的几率是\(\frac{1}{m}\),插入一个元素须要插入k个hash值,因此插入一个元素,某一位没有被置为1的几率是\((1-\frac{1}{m})^k\)。插入n个元素以后,某一位依旧为0的几率是\((1-\frac{1}{m})^{nk}\),它变成1的几率是\(1-(1-\frac{1}{m})^{nk}\)

若是在某次判断当中,有一个没有出现过的元素被认为已经在集合当中了,那么也就是说它hash获得的位置均已经在以前被置为1了,这个时间发生的几率为:

\[\displaystyle\left[1-(1-\frac{1}{m})^{nk}\right]^k \approx (1-e^{-\frac{kn}{m}})^k\]

这里用到了一个极限:

\[\displaystyle\lim_{x \to -\infty}(1-\frac{1}{x})^{-x}=e\]

咱们来求一下冲突率最低时k的取值,为了方便计算,咱们令\(b=e^{\frac{n}{m}}\),代入:

\[f(k) = (1-b^{-k})^k \\ \ln f(k) = k\ln(1-b^{-k})\]
两边求导:

\[ \begin{aligned} \frac{1}{f(k)}f'(k)&= ln(1-b^{-k}) + \frac{kb^{-k}\ln b}{1-b^{-k}} \end{aligned} \]

咱们令导数等于0,来求它的极值:

\[ \begin{aligned} \ln(1-b^{-k})(1-b^{-k})&=-kb^{-k}\ln b\\ \ln(1-b^{-k})(1-b^{-k})&=b^{-k}\ln b^{-k}\\ 1-b^{-k} &=b^{-k}\\ b^{-k} &= \frac{1}{2} \end{aligned} \]

咱们将\(b^{-k}=\frac{1}{2}\)代入,能够求出最值时的\(k=\ln2\cdot\frac{m}{n} \approx 0.7\frac{m}{n}\)

同理,咱们也能够预设定集合元素n和错判率p,来求解对应的n,一样利用上面的公式推算,能够获得\(m=-\frac{n\ln p}{(\ln2)^2}\)

若是咱们容许必定的容错,而且可以大概估计会出现的元素的个数,那么彻底可使用布隆过滤器来代替传统的容器判重的方法。这样不只效率极高,并且对于存储的要求很是小。


灵魂拷问


原理也明白了,代码也看懂了,这个时候咱们来思考一个问题:布隆过滤器能够删除元素吗?

很遗憾,布隆过滤器是不支持删除的。

由于布隆过滤器的每个bit并非独占的,颇有可能多个元素共享了某一位。若是咱们直接删除这一位的话,会影响其余的元素。

仍是用上面的例子举例:咱们删除线性代数,线性代数对应的位置是1,3,5,虽然咱们并无删除高等数学,可是因为咱们移除了高等数学也用到的位置1,若是咱们再去判断高等数学是否存在就会获得错误的结果,虽然咱们并无删除它。

固然,在一些必需要有删除功能的场景下,也是有办法的。方法也很简单,就是修改数据结构,将本来每一位一个bit改为一个int,当咱们插入元素的时候,再也不是将bit设置为true,而是让对应的位置自增,而删除的时候则是对应的位减一。这样,咱们删除单个结果就不会影响其余元素了。

这种方法并非完美的,因为布隆过滤器存在误判的状况,颇有可能咱们会删除本来就不存在的值,这一样会对其余元素产生影响。

布隆过滤器是一个优缺点都很是明显的数据结构,优势很是出色:速度足够快,内存消耗小,代码实现简单。可是缺点也很明显:不支持删除元素,会有误判的状况。这样特色鲜明的数据结构真的很是吸引人。

今天的文章就是这些,若是以为有所收获,请顺手点个关注吧,大家的举手之劳对我来讲很重要。