布隆过滤器及其实现

简介

(Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出的。loom Filter是一种空间效率很高的随机数据结构,它实际上是由一个很长的二进制向量和一系列随机映射函数组成,布隆过滤器可以用于可以快速且空间效率高的判断一个元素是否属于一个集合;用来实现数据字典,或者集合求交集

  • 优点:
    • 空间效率和查询时间都远远超过一般算法;
  • 缺点:
    • 有一定的误识别率(假正例False positives,即Bloom Filter报告某一元素存在于某集合中,但是实际上该元素并不在集合中)和删除困难,但是没有识别错误的情况(即假反例False negatives,如果某个元素确实没有在该集合中,那么Bloom Filter 是不会报告该元素存在于集合中的,所以不会漏报);

Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。

在日常生活中,包括在设计计算机软件时,我们经常要判断一个元素是否在一个集合中。比如在字处理软件中,需要检查一个英语单词是否拼写正确(也就是要判断它是否在已知的字典中);

在 FBI,一个嫌疑人的名字是否已经在嫌疑名单上;在网络爬虫里,一个网址是否被访问过等等。最直接的方法就是将集合中全部的元素存在计算机中,遇到一个新 元素时,将它和集合中的元素直接比较即可。

一般来讲,计算机中的集合是用哈希表(hash table)来存储的,其好处是快速准确,缺点是费存储空间。当集合较小时,这个问题不显著,但是当集合巨大时,哈希表的存储效率低就显现出来了。

比如说,一个象 Yahoo,Hotmail 和 Gmai 那样的公众电子邮件(email)提供商,总是需要过滤来自发送垃圾邮件的人(spamer)的垃圾邮件。一个办法就是记录下那些发垃圾邮件的 email 地址。由于那些发送者不停地在注册新的地址,全世界少说也有几十亿个发垃圾邮件的地址,将他们都存起来则需要大量的网络服务器。如果用哈希表,每存储一亿 个 email 地址, 就需要 1.6GB 的内存(用哈希表实现的具体办法是将每一个 email 地址对应成一个八字节的信息指纹,然后将这些信息指纹存入哈希表,由于哈希表的存储效率一般只有 50%,因此一个 email 地址需要占用十六个字节。一亿个地址大约要 1.6GB, 即十六亿字节的内存)。因此存贮几十亿个邮件地址可能需要上百 GB 的内存。除非是超级计算机,一般服务器是无法存储的。

概念

集合表示和元素查询

下面我们具体来看Bloom Filter是如何用位数组表示集合的。初始状态时,Bloom Filter是一个包含m位的位数组,每一位都置为0。

这里写图片描述

为了表达S={x1, x2,…,xn}这样一个n个元素的集合,Bloom Filter使用k个相互独立的哈希函数(Hash Function),它们分别将集合中的每个元素映射到{1,…,m}的范围中。对任意一个元素x,第i个哈希函数映射的位置hi(x)就会被置为1(1≤i≤k)。注意,如果一个位置多次被置为1,那么只有第一次会起作用,后面几次将没有任何效果。在下图中,k=3,且有两个哈希函数选中同一个位置(从左边数第五位)。

这里写图片描述

在判断y是否属于这个集合时,我们对y应用k次哈希函数,如果所有hi(y)的位置都是1(1≤i≤k),那么我们就认为y是集合中的元素,否则就认为y不是集合中的元素。下图中y1就不是集合中的元素。y2或者属于这个集合,或者刚好是一个false positive。

这里写图片描述

如果想判断一个元素是不是在一个集合内,一般想到的是将所有元素保存起来,然后通过比较确定。链表,数等数据结构都是这种思路。但是随着集合中元素的增加,需要的存储空间越来越大,检索速度越来越慢。

不过世界上还有一种叫作散列表(又叫哈希表,Hash table)的数据结构。它可以通过一个Hash函数将一个元素映射到一个位阵列中的一个点,这样一来我们只要看看这个点是不是 1就知道可以集合中有没有它了。这就是布隆过滤器的基本思想。

Hash面临的问题是冲突。假设Hash函数是良好的,如果我们的位阵列长度为m个点,那么如果想将冲突率降低到1%,这个散列表只能容纳m/100个元素,显然这就不叫空间有效了,解决的方法也很简单,就是使用多个Hash函数,如果它们有一个说元素不在集合中,那肯定就不在,如果它们都说在,虽然有一定可能性它们在说谎,不过直觉上判断这种事情的概率还是比较低的。

这里写图片描述

原理详解

代码实现

bitmap.h

#ifndef _BIT_MAP_H
#define _BIT_MAP_H

#include<iostream>
#include<vector>

using namespace std;

class BitMap{

public:
    vector<size_t> _array;  //数组,存储位图
public:
    BitMap(){}
    BitMap(size_t size){
        _array.resize((size >> 5) + 1);  //位图的大小
    }

    bool test(size_t num){
        return _array[num >> 5] & (1 << (num & 0x1F));
    }

    void Set(size_t num){
        if (!test(num)){
            _array[num >> 5] |= (1 << (num & 0x1F));
        }
    }

    void ClearSet(size_t num){
        _array[num >> 5] &= ~(1 << (num & 0x1F));
    }

    void ReSet(size_t num){
        if (test(num)){
            ClearSet(num);
        }
    }

    void showBitMap(){
        cout << "BitMap 位图展示:" << endl;
        for (int i = 0; i < _array.size(); i++){
            if (_array[i]){
                vector<int> res;
                size_t tmp = _array[i];
                for (int i = 0; i < 32; i++){
                    res.push_back(tmp & 1);
                    tmp >>= 1;
                }
                cout << "_array[" << i << "]->";
                for (int i = 0; i < res.size(); i++){
                    cout << res[i] << " ";
                }
                cout << endl;
            }
        }
    }
};

#endif

HashFun.h

#pragma once 

//各类哈希函数
template<class T>
size_t BKDRHash(const char* str){
    register size_t hash = 0;
    while (size_t ch = (size_t)*str++){
        hash = hash * 131 + ch;
    }
    return hash;
}

template<class T>
size_t SDBMHash(const char *str)
{
    register size_t hash = 0;
    while (size_t ch = (size_t)*str++)
    {
        hash = 65599 * hash + ch;
    }
    return hash;
}

template<class T>
size_t RSHash(const char * str)
{
    size_t hash = 0;
    size_t magic = 63689;
    while (size_t ch = (size_t)*str++)
    {
        hash = hash * magic + ch;
        magic *= 378551;
    }
    return hash;
}


template<class T>
size_t APHash(const char *str)
{
    register size_t hash = 0;
    size_t ch;
    for (long i = 0; ch = (size_t)*str++; i++)
    {
        if ((i & 1) == 0)
        {
            hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
        }
        else
        {
            hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
        }
    }
    return hash;
}

template<class T>
size_t JSHash(const char* str)
{
    if (!*str)
    {
        return 0;
    }
    size_t hash = 1315423911;
    while (size_t ch = (size_t)*str++)
    {
        hash ^= ((hash << 5) + ch + (hash >> 2));
    }
    return hash;
}

Bloom_Filter.h

#pragma once  
#include "bitmap.h"
#include "HashFun.h"
#include <string>

template<class T>
//对应哈希函数的仿函数
struct __HashFun1{
    size_t operator()(const T& key){
        return BKDRHash<T>(key.c_str());
    }
};

template<class T>
struct __HashFun2
{
    size_t operator()(const T& key)
    {
        return SDBMHash<T>(key.c_str());
    }
};

template<class T>
struct __HashFun3{
    size_t operator()(const T& key)
    {
        return RSHash<T>(key.c_str());
    }
};

template<class T>
struct __HashFun4{
    size_t operator()(const T& key)
    {
        return APHash<T>(key.c_str());
    }
};

template<class T>
struct __HashFun5{
    size_t operator()(const T& key)
    {
        return JSHash<T>(key.c_str());
    }
};

template < class K = string,
class HashFun1 = __HashFun1<K>,
class HashFun2 = __HashFun2<K>,
class HashFun3 = __HashFun3<K>,
class HashFun4 = __HashFun4<K>,
class HashFun5 = __HashFun5<K>>
class Bloom_Filter{
private:
    BitMap *_bitmap;
    size_t _capacity;

public:

    Bloom_Filter(size_t size) :_capacity(size){
        _bitmap = new BitMap(size);
    }

    ~Bloom_Filter(){
        delete _bitmap;
    }

    void set(const K& key){
        _bitmap->Set(HashFun1()(key) % _capacity);
        _bitmap->Set(HashFun2()(key) % _capacity);
        _bitmap->Set(HashFun3()(key) % _capacity);
        _bitmap->Set(HashFun4()(key) % _capacity);
        _bitmap->Set(HashFun5()(key) % _capacity);
    }

    bool isin(const K& key){
        if (!_bitmap->test(HashFun1()(key) % _capacity))
            return false;
        if (!_bitmap->test(HashFun2()(key) % _capacity))
            return false;
        if (!_bitmap->test(HashFun3()(key) % _capacity))
            return false;
        if (!_bitmap->test(HashFun4()(key) % _capacity))
            return false;
        if (!_bitmap->test(HashFun5()(key) % _capacity))
            return false;

        return true;
    }

    void show(){
        _bitmap->showBitMap();
    }

};

main.cpp

#include "Bloom_Filter.h"
#include<fstream>

using namespace std;

int main(){

    ifstream  infd("test.txt");
    string str;
    Bloom_Filter<string> bf(150000);
    while (getline(infd, str)){
        bf.set(str);
    }
    string tmp;
    while (getline(cin, str)){
        if (bf.isin(str)){
            cout << str << " is in Bloom Filter" << endl;
        }
        else{
            cout << str << " is NOT in Bloom Filter..." << endl;
        }
    }
    //bf.show();
    system("pause");
    return 0;
}

布隆过滤器的优缺点

优点:

  • 相比于其他数据结构,布隆过滤器在空间和时间方面都有巨大的优势,布隆过滤器存储、插入、查询都是常数时间;
  • hash函数相互之间没有关系,方便由硬件并行实现;
  • 布隆过滤器不需要存储元素本身,在某些对保密要求严格的场合有优势;
  • 其可以表示全集,其他数据结构不能;
  • k,m(k哈希函数个数,m范围)相同,使用同一组Hash函数的两个布隆过滤器的叫并差运算可以使用位操作进行;

缺点:

  • 随着存入元素的增加,误算率随之增加。如果元素数量太少,则使用散列表足矣;
  • 一般情况下不能从布隆过滤器中删除元素;
    • 我们很容易想到把位列阵变成整数数组,每插入一个元素相应的计数器加1, 这样删除元素时将计数器减掉就可以了。然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面. 这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。

应用示例

电话号码应用示例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值