布隆过滤器

很多数据库存储的底层实现方案中都采用了布隆过滤器,好好的学习下吧.

这篇文章来自
<博客园> 布隆过滤器
<\CSDN> 初步认识bloom filter(布隆过滤器)以及java实现代码

  假如有1亿个不重复的正整数(大致范围已知),但是只有1G的内存可用,如何判断该范围内的某个数是否出现在这1亿个数中?最常用的处理办法是利用位图,1*108/1024*1024*8=11.9,也只需要申请12M的内存。但是如果是1亿个邮件地址,如何确定某个邮件地址是否在这1亿个地址中?这个时候可能大家想到的最常用的办法就是利用Hash表了,但是大家可以细想一下,如果利用Hash表来处理,必须开辟空间去存储这1亿个邮件地址,因为在Hash表中不可能避免的会发生碰撞,假设一个邮件地址只占8个字节,为了保证Hash表的碰撞率,所以需要控制Hash表的装填因子在0.5左右,那么至少需要2*8*108/1024*1024*1024=1.5G的内存空间,这种情况下利用Hash表是无法处理的。这个时候要用到另外一种数据结构-布隆过滤器(Bloom Filter),它是由Burton Howard Bloom在1970年提出的,它结合了位图和Hash表两者的优点,位图的优点是节省空间,但是只能处理整型值一类的问题,无法处理字符串一类的问题,而Hash表却恰巧解决了位图无法解决的问题,然而Hash太浪费空间。针对这个问题,布隆提出了一种基于二进制向量和一系列随机函数的数据结构-布隆过滤器。它的空间利用率和时间效率是很多算法无法企及的,但是它也有一些缺点,就是会有一定的误判率并且不支持删除操作。

  下面来讨论一下布隆过滤器的原理和它的应用。

一.布隆过滤器的原理

  布隆过滤器需要的是一个位数组(这个和位图有点类似)和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函数,具体可以参考:《常见的Hash算法》这篇博文,里面列举了很多常见的Hash函数。并且可以证明布隆过滤器的误判率和位数组的大小以及映射函数的个数有关,相关证明可参考这篇博文:《布隆过滤器 (Bloom Filter) 详解》。假设误判率为p,位数组大小为m,集合数据个数为n,映射函数个数为k,它们之间的关系如下:

  p=2-(m/n)*ln2 可得 m=(-n*lnp)/(ln2)2=-2*n*lnp=2*n*ln(1/p)

  k=(m/n)ln2=0.7(m/n)

  可以验证若p=0.1,(m/n)=9.6,即存储每个元素需要9.6bit位,此时k=0.7*(m/n)=6.72,即存储每个元素需要9.6个bit位,其中有6.72个bit位被置为1了,因此需要7个映射函数。从这里可以看出布隆过滤器的优越性了,比如上面例子中的,存储一个邮件地址,只需要10个bit位,而用hash表存储需要8*8=64个bit位。

  一般情况下,p和n由用户设定,然后根据p和n的值设计位数组的大小和所需的映射函数的个数,再根据实际情况来设计映射函数。

  尤其要注意的是,布隆过滤器是不允许删除元素的,因为若删除一个元素,可能会发生漏判的情况。不过有一种布隆过滤器的变体Counter Bloom Filter,可以支持删除元素,感兴趣的读者可以查阅相关文献资料。

二.布隆过滤器的应用

  布隆过滤器在很多场合能发挥很好的效果,比如:网页URL的去重,垃圾邮件的判别,集合重复元素的判别,查询加速(比如基于key-value的存储系统)等,下面举几个例子:

  1.有两个URL集合A,B,每个集合中大约有1亿个URL,每个URL占64字节,有1G的内存,如何找出两个集合中重复的URL。

  很显然,直接利用Hash表会超出内存限制的范围。这里给出两种思路:

  第一种:如果不允许一定的错误率的话,只有用分治的思想去解决,将A,B两个集合中的URL分别存到若干个文件中{f1,f2…fk}和{g1,g2….gk}中,然后取f1和g1的内容读入内存,将f1的内容存储到hash_map当中,然后再取g1中的url,若有相同的url,则写入到文件中,然后直到g1的内容读取完毕,再取g2…gk。然后再取f2的内容读入内存。。。依次类推,知道找出所有的重复url。

  第二种:如果允许一定错误率的话,则可以用布隆过滤器的思想。

  2.在进行网页爬虫时,其中有一个很重要的过程是重复URL的判别,如果将所有的url存入到数据库中,当数据库中URL的数量很多时,在判重时会造成效率低下,此时常见的一种做法就是利用布隆过滤器,还有一种方法是利用berkeley db来存储url,Berkeley db是一种基于key-value存储的非关系数据库引擎,能够大大提高url判重的效率。
  

应用

1. 垃圾邮件过滤中的黑白名单
2. 爬虫(Crawler)的网址判重模块

下面详细的阐述一个例子,先是插入:
1. 为了存储一亿个电子邮件地址
2. 建立一个含有十六亿二进制比特,也就是两亿字节
3. 将十六亿的比特全部设置为0
4. 我们用八个不同的哈希函数,以电子邮件地址为键,算出值,有8个(也许不是数字)
5. 我们再一个哈希函数,分别以这8个值为键,会得到8个数值(范围为1到十六亿的某个数字)
6. 以上一步算出的8个数值为下标,将这8个位置的二进制都设置为1(存储了一个地址)
  查询时只需要用类似的方法得到相应电子邮件的8个数值,以其为下标看二进制是否都设置为了1,如果设置为了1,那么这个电子邮件就存在在这个表中。

优点
1.具有很好的空间和时间效率(只需要哈希表的1/8或1/4的空间复杂度就能完成同样的问题)
2.不存在false negative (漏报),就是说如果元素存在的话,必能得到正确的结果

缺点
1.不能删除已储存的元素
2.元素越多,false positive rate(误报率)越大,也就说将不存在的元素判定为存在。(常见的补救方法:增加一个白名单,存储可能被误判的元素)

C++ 实现

#include<iostream>
#include<bitset>
#include<string>
#define MAX 2<<24
using namespace std;

bitset<MAX> bloomSet;           //简化了由n和p生成m的过程 

int seeds[7]={3, 7, 11, 13, 31, 37, 61};     //使用7个hash函数 

int getHashValue(string str,int n)           //计算Hash值 
{
    int result=0;
    int i;
    for(i=0;i<str.size();i++)
    {
        result=seeds[n]*result+(int)str[i];
        if(result > 2<<24)
            result%=2<<24;
    }
    return result;
}

bool isInBloomSet(string str)                //判断是否在布隆过滤器中 
{
    int i;
    for(i=0;i<7;i++)
    {
        int hash=getHashValue(str,i);
        if(bloomSet[hash]==0)
            return false;
    }
    return true;
}

void addToBloomSet(string str)               //添加元素到布隆过滤器 
{
    int i;
    for(i=0;i<7;i++)
    {
        int hash=getHashValue(str,i);
        bloomSet.set(hash,1);
    }
}

void initBloomSet()                         //初始化布隆过滤器 
{
    addToBloomSet("http://www.baidu.com");
    addToBloomSet("http://www.cnblogs.com");
    addToBloomSet("http://www.google.com");
}

int main(int argc, char *argv[])
{

    int n;
    initBloomSet();
    while(scanf("%d",&n)==1)
    {
        string str;
        while(n--)
        {
            cin>>str;
            if(isInBloomSet(str))
                cout<<"yes"<<endl;
            else
                cout<<"no"<<endl;
        }

    }
    return 0;
}

Java实现

import java.util.BitSet;
/**
 * 就是这个过滤器,有插入、查询等功能,可以设置位集的大小。虽然有删除功能,但是最好不要用
 * @author chouyou
 *
 */
public class bloomFilter {
    private int defaultSize = 5000 << 10000;// <<是移位运算
    /**
     * 从basic的使用来看,hashCode最后的结果会产生一个int类型的数,而这个int类型的数的范围就是0到baisc
     * 所以basic的的值为defaultsize减一
     */
    private int basic = defaultSize -1;

    private BitSet bits = new BitSet(defaultSize);//初始化一个一定大小的位集

    public bloomFilter(){
    }
    /**
     * 针对一个key,用8个不同的hash函数,产生8个不同的数,数的范围0到defaultSize-1
     * 以这个8个数为下标,将位集中的相应位置设置成1
     * @return
     */
    private int[] indexInSet(element ele){
        int[] indexes = new int[8];
        for (int i = 0;i<8;i++){
            indexes[i] = hashCode(ele.getKey(),i);
        }
        return indexes;
    }
    /**
     * 添加一个元素到位集内
     */
    private void add(element ele){
        if(exist(ele)){
            System.out.println("已经包含("+ele.getKey()+")");
            return;
        }
        int keyCode[] = indexInSet(ele);
        for (int i = 0;i<8;i++){
            bits.set(keyCode[i]);
        }
    }
    /**
     * 判断是否存在
     * @return
     */
    private boolean exist(element ele){
        int keyCode[] = indexInSet(ele);
        if(bits.get(keyCode[0])
                &&bits.get(keyCode[1])
                &&bits.get(keyCode[2])
                &&bits.get(keyCode[3])
                &&bits.get(keyCode[4])
                &&bits.get(keyCode[5])
                &&bits.get(keyCode[6])
                &&bits.get(keyCode[7])){
            return true; 
        }
        return false;
    }
    /**
     * 要进行集合删除某个元素
     * 那么在位集中将相应的下标设置为0即可
     * 但是这样岂不是有可能会让影响到别的元素,因为多个元素公用一个下标呀
     * 那样岂不是让别的元素也不存在了么
     * 经查证,这就是bloom Filter的缺点,不能删除元素。
     * @return
     */
    private boolean deleteElement(element ele){
        if(exist(ele)){
            int keyCode[] = indexInSet(ele);
            for (int i = 0;i<8;i++){
                bits.clear(keyCode[i]);
            }
            return true;
        }
        return false;
    }
    /**
     * Q传入不同的Q就可以得到简单的不同的hash函数
     */
    private int hashCode(String key,int Q){
        int h = 0;
        int off = 0;
        char val[] = key.toCharArray();
        int len = key.length();
        for (int i = 0; i < len; i++) {
            h = (30 + Q) * h + val[off++];
        }
        return changeInteger(h);
    }

    private int changeInteger(int h) {
        return basic & h;//&是位与运算符
    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        bloomFilter f=new bloomFilter();
        element ele = new element("blog.csdn.net/zy825316");
        System.out.println("位集大小:"+f.defaultSize);
        f.add(ele);
        System.out.println(f.exist(ele));
        f.deleteElement(ele);
        System.out.println(f.exist(ele));
    }
}
/** 
 * 位集里面的每一个元素 
 * @author chouyou 
 * 
 */  
public class element {  
    private String key = null;  
    public element(String key){  
        this.setKey(key);  
    }  
    public String getKey() {  
        return key;  
    }  
    public void setKey(String key) {  
        this.key = key;  
    }  
}  
  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值