看见了海量数据去重,找到停留时间最长的IP等问题,有博友提到了Bloom Filter,我就查了查,不过首先想到的是大叔,下面就先看看大叔的风采。java
1、布隆过滤器概念引入算法
(Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出的。它其实是由一个很长的二进制向量和一系列随机映射函数组成,布隆过滤器能够用于检索一个元素是否在一个集合中。它的优势是空间效率和查询时间都远远超过通常的算法,缺点是有必定的误识别率(假正例False positives,即Bloom Filter报告某一元素存在于某集合中,可是实际上该元素并不在集合中)和删除困难,可是没有识别错误的情形(即假反例False negatives,若是某个元素确实在该集合中,那么Bloom Filter 是不会报告该元素不存在于集合中的,因此不会漏报)。数据库
下面从简单的排序谈到BitMap算法,再谈到数据去重问题,谈到大数据量处理利器:布隆过滤器。网页爬虫
给定数据(2,4,1,12,9,7,6)如何对它排序?数组
方法1:基本的排序方法包括冒泡,快排等。函数
方法2:使用BitMap算法大数据
方法1就不介绍了,方法2中所谓的BitMap是一个位数组,跟平时使用的数组的惟一差异在于操做的是位。首先是开辟2个字节大小的位数组,长度为12(该长度由上述数据中最大的数字12决定的),而后,读取数据,2存放在位数组中下标为1的地方,值从0改成1,4存放在下标为3的地方,值从0改成1....最后,读取该位数组,获得排好序的数据是:(1,2,4,6,7,9,12)。this
比较方法1和方法2的差异:方法2中,排序须要的时间复杂度和空间复杂度很依赖与数据中最大的数字好比12,所以空间上讲须要开2个字节大小的内存,时间上须要遍历完整个数组。当数据相似(1,1000,10万)只有3个数据的时候,显然用方法2,时间复杂度和空间复杂度至关大,可是当数据比较密集时该方法就会显示出来优点。url
数据(2,4,1,12,2,9,7,6,1,4)如何找出重复出现的数字?spa
首先是开辟2个字节大小的位数组,长度为12(该长度由上述数据中最大的数字12决定的,当读取完12后,当读取2的时候,发现数组中的值是1,则判断出2是重复出现的。
2、布隆过滤器原理
布隆过滤器须要的是一个位数组(这个和位图有点相似)和k个映射函数(和Hash表相似),在初始状态时,对于长度为m的位数组array,它的全部位都被置为0。对于有n个元素的集合S={s1,s2......sn},经过k个映射函数{f1,f2,......fk},将集合S中的每一个元素sj(1<=j<=n)映射为k个值{g1,g2......gk},而后再将位数组array中相对应的array[g1],array[g2]......array[gk]置为1;若是要查找某个元素item是否在S中,则经过映射函数{f1,f2.....fk}获得k个值{g1,g2.....gk},而后再判断array[g1],array[g2]......array[gk]是否都为1,若全为1,则item在S中,不然item不在S中。这个就是布隆过滤器的实现原理。
固然有读者可能会问:即便array[g1],array[g2]......array[gk]都为1,能表明item必定在集合S中吗?不必定,由于有这个可能:就是集合中的若干个元素经过映射以后获得的数值恰巧包括g1,g2,.....gk,那么这种状况下可能会形成误判,可是这个几率很小,通常在万分之一如下。
很显然,布隆过滤器的误判率和这k个映射函数的设计有关,到目前为止,有不少人设计出了不少高效实用的hash函数。尤为要注意的是,布隆过滤器是不容许删除元素的(实际就是由于多个str可能都应设在同一点,而判断str存在的话是全部映射点都存在,因此不能删除),由于若删除一个元素,可能会发生漏判的状况。不过有一种布隆过滤器的变体Counter Bloom Filter,能够支持删除元素,感兴趣的读者能够查阅相关文献资料。
3、布隆过滤器False positives 几率推导
假设 Hash 函数以等几率条件选择并设置 Bit Array 中的某一位,m 是该位数组的大小,k 是 Hash 函数的个数,那么位数组中某一特定的位在进行元素插入时的 Hash 操做中没有被置位为1的几率是:;那么在全部 k 次 Hash 操做后该位都没有被置 "1" 的几率是:
;若是咱们插入了 n 个元素,那么某一位仍然为 "0" 的几率是:
于是该位为 "1"的几率是:
;如今检测某一元素是否在该集合中。标明某个元素是否在集合中所需的 k 个位置都按照如上的方法设置为 "1",可是该方法可能会使算法错误的认为某一本来不在集合中的元素却被检测为在该集合中(False Positives),该几率由如下公式肯定:
。
其实上述结果是在假定由每一个 Hash 计算出须要设置的位(bit) 的位置是相互独立为前提计算出来的,不难看出,随着 m (位数组大小)的增长,假正例(False Positives)的几率会降低,同时随着插入元素个数 n 的增长,False Positives的几率又会上升,对于给定的m,n,如何选择Hash函数个数 k 由如下公式肯定:;此时False Positives的几率为:
;而对于给定的False Positives几率 p,如何选择最优的位数组大小 m 呢,
;该式代表,位数组的大小最好与插入元素的个数成线性关系,对于给定的 m,n,k,假正例几率最大为:
。
4、布隆过滤器应用
布隆过滤器在不少场合能发挥很好的效果,好比:网页URL的去重,垃圾邮件的判别,集合重复元素的判别,查询加速(好比基于key-value的存储系统)等,下面举几个例子:
很显然,直接利用Hash表会超出内存限制的范围。这里给出两种思路:
第一种:若是不容许必定的错误率的话,只有用分治的思想去解决,将A,B两个集合中的URL分别存到若干个文件中{f1,f2...fk}和{g1,g2....gk}中,而后取f1和g1的内容读入内存,将f1的内容存储到hash_map当中,而后再取g1中的url,如有相同的url,则写入到文件中,而后直到g1的内容读取完毕,再取g2...gk。而后再取f2的内容读入内存。。。依次类推,知道找出全部的重复url。
第二种:若是容许必定错误率的话,则能够用布隆过滤器的思想。
量很多时,在判重时会形成效率低下,此时常见的一种作法就是利用布隆过滤器,还有一种方法是利用berkeley db来存储url,Berkeley db是一种基于key-value存储的非关系数据库引擎,可以大大提升url判重的效率。
布隆过滤器主要运用在过滤恶意网址用的,将全部的恶意网址创建在一个布隆过滤器上,而后对用户的访问的网址进行检测,若是在恶意网址中那么就通知用户。这样的话,咱们还能够对一些常出现判断错误的网址设定一个白名单,而后对出现判断存在的网址再和白名单中的网址进行匹配,若是在白名单中,那么就放行。固然这个白名单不能太大,也不会太大,布隆过滤器错误的几率是很小的。
5、布隆过滤器简单Java实现
package a; import java.util.BitSet; /* * 存在的问题 * DEFAULT_LEN长度设置为多少合适呢? * 我发现result和DEFAULT_LEN有关,不该该啊,没发现缘由 */ public class BloomFilterTest { //30位,表示2^2^30种字符 static int DEFAULT_LEN = 1<<30; //要用质数 static int[] seeds = {3,5,7,11,17,31}; static BitSet bitset = new BitSet(DEFAULT_LEN); static MyHash[] myselfHash = new MyHash[seeds.length]; public static void main(String[] args) { String str = "791909235@qq.com"; //生成一次就够了 for(int i=0; i<seeds.length; i++) { myselfHash[i] = new MyHash(DEFAULT_LEN, seeds[i]); } bitset.clear(); for(int i=0; i<myselfHash.length; i++) { bitset.set(myselfHash[i].myHash(str),true); } boolean flag = containsStr(str); //System.out.println("========================"); System.out.println(flag); } private static boolean containsStr(String str) { // TODO Auto-generated method stub if(null==str) return false; for(int i=0; i<seeds.length; i++) { if(bitset.get(myselfHash[i].myHash(str))==false) return false; } return true; } } class MyHash { int len; int seed; public MyHash(int len, int seed) { super(); this.len = len; this.seed = seed; } public int myHash(String str) { int len = str.length(); int result = 0; //这的len就是str的len,不是成员变量的len for(int i=0; i<len; i++) { //System.out.println(seed+"oooooooooooo"); result = result*seed + str.charAt(i); //System.out.println(result); //长度就是1<<24,若是大于这个数 感受结果不许确 //<0就是大于了0x7ffffff if(result>(1<<30) || result<0) { //System.out.println("-----"+(1<<30)); System.out.println(result+"myHash数据越界!!!"); break; } } return (len-1)&result; } }