BloomFilter,Cuckoo Filter的实现

Bloom Filter

数字的话,位图是可以处理的,因为他们是不存在冲突的,每一个数字对于一个唯一的哈希地址;但是对于字符串,位图是没有办法处理的,因为字符串存在哈希冲突。因此,我们只能通过我们将一个元素经过多个散列函数映射到多个位上来降低误判的概率,布隆过滤器正是基于这个特点应用而生的。

Bloom Filter是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。Bloom Filter的这种高效是有一定代价的:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。因此,Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。

错误率估计

把不属于这个集合的元素误认为属于这个集合,这样出错的概率称为错误率(false positive rate

n个元素的集合,Bloom Filter使用k个相互独立的哈希函数,把他们映射到大小为m的空间中。在估计之前为了简化模型,我们假设kn<m且各个哈希函数是完全随机的。当集合S={x1, x2,…,xn}的所有元素都被k个哈希函数映射到m位的位数组中时,这个位数组中某一位还是0的概率是:


p ′ = ( 1 − 1 m ) k n ≈ e − k n / m p'=(1-\frac{1}{m})^{kn}≈e^{-kn/m} p=(1m1)knekn/m
因为每次映射到该点的概率为1/m,则不映射到该点的概率为(1-1/m),由于n个元素一共要重复kn次,所以得到上面式子

令ρ为位数组中0的比例,则ρ的数学期望E(ρ)= p’,位数组中0的比例非常集中地分布在它的数学期望值的附近。则(1-p’)为位数中为1的比例,(1-ρ)k就表示k次哈希都刚好选中1的区域,即错误率
f = ( 1 − e − k n / m ) k = ( 1 − p ) k f=(1-e^{-kn/m})^k=(1-p)^k f=(1ekn/m)k=(1p)k

表明k值的选取,对错误率有影响

最优哈希函数个数K

要达到最优,则应该使得错误率最低,由于
f = e x p ( k l n ( 1 − e − k n / m ) ) f= exp(kln(1−e^{−kn/m})) f=exp(kln(1ekn/m))
所以令
g = k l n ( 1 − e − k n / m ) g=kln(1−e^{−kn/m}) g=kln(1ekn/m)
只要g取到最小,那么f也最小。由于
p = e − k n / m , 我 们 可 以 把 g 写 成 g = − m n l n p l n ( 1 − p ) p=e^{-kn/m},我们可以把g写成g=-\frac{m}{n}lnpln(1-p) p=ekn/mgg=nmlnpln(1p)
根据对称法则容易得出p=1/2,即k= ln2· (m/n),g取得最小值。P=1/2,对应着位数0和1各一半,要保持最低错误率,应该使数组有一半空着

增加删除功能

标准的Bloom Filter只支持插入和查找操作,不能进行删除,因为删除的话把相应位全部置为0,此时可能产生误判。为了支持删除操作,我们把它变为Counting Bloom Filter。它将标准 Bloom Filter 位数组的每一位扩展为一个小的计数器(Counter),在插入元素时给对应的 k (k 为哈希函数个数)个 Counter 的值分别加 1,删除元素时给对应的 k 个 Counter 的值分别减 1

aFKttH.jpg

缺点

  1. 不支持反向删除元素:一旦对位数组进行了赋值,无法将其删除。
  2. 查询性能弱:布隆过滤器使用多个hash函数计算位图多个不同位点,由于多个位点在内存中不连续,CPU寻址花销较大。
  3. 空间利用率低。

Bloom Filter的实现

结构体:

typedef struct BloomFilter //封装一个位图
{ char* a;//也可以用其他的类型开辟数据
size_t asize;//位个数
BloomFunc *funcs;
int nfuncs;
}BloomFilter;

初始化过滤器

void BloomFilterInit(BloomFilter* pbf,size_t n,int k, …) //初始化布隆过滤器
{
assert(pbf);
va_list l;
pbf->asize = n;
//这里一定要加1,假如n为33,33/8=4,但是要5个字节才能存的下
size_t size = n/8 + 1;
//开辟空间
pbf->a =(char*)malloc(sizesizeof(char));
pbf->funcs=(BloomFunc
)malloc(k*sizeof(BloomFunc));
//初始化为0
memset(pbf->a,0,size);
va_start(l, k);
for(int i=0; i<k; ++i) {
pbf->funcs[i]=va_arg(l,BloomFunc);
}
va_end(l);
pbf->nfuncs=k;
}

c语言提供了函数的不定长参数使用,比如 上面的void BloomFilterInit(BloomFilter* pbf,size_t n,int k, …)。三个省略号,表示了不定长参数。但是C标准规定函数必须至少有一个明确定义的参数,因此,省略号前面必须有至少一个参数。va_start(l,k)使得定义的指针指向了不定长参数列表省略号前的参数,va_arg(va_list, type),获取参数列表的下一个参数,并以type的类型返回。va_end()用于收回参数列表指针,否则出现野指针。

为了使得过滤器拥有最低的错误率,所以在测试函数中,我们根据插入的数据的个数,应用公式k= ln2· (m/n),可以得到最优的K值。然后在初始化bloomfilter函数中,利用for循环,以及va_list(),va_start(),va_arg(),va_end()函数的配套使用,可以动态地设置BloomFilter结构体中哈希函数的个数。以此来实现根据m,n (或误报率)要求自动设置参数K。

插入数据

void BloomFilterSet(BloomFilter* pbf,char* str) //将要放置的内容映射的地址设置为1
{
assert(pbf&&str);
size_t len=strlen(str);
int i=0;
size_t index[FUNCMAXSIZE]={0};
//根据哈希函数计算哈希地址(将字符串转换为整数)
for(i;infuncs;i++)
{ index[i]=pbf->funcsi%(pbf->asize);
Set(pbf,index[i]);
}
}

void Set(BloomFilter* pbf,size_t x) //设置要存的值位置为1
{

​ assert(pbf);
​ int index=x/8;//计算在第几个char里,相当于/8
​ int num=x%8;//计算在第几位
​ pbf->a[index]|=(1<<num);

}

使用void BloomFilterSet()和void Set()函数来实现数据的插入,方法是通过for循环,依次调用哈希函数,来计算出哈希值,对哈希值进行取余,就能得到对应位的值,然后在调用set()函数,计算出在数组中的位置,然后再将该数组对应的位由0变为1,方法就是利用”或“位运算pbf->a[index]|=(1<<num)。

查找数据

int BloomFilterTest(BloomFilter* pbf,char* str) //判断一个字符串是否存在(存在返回1,不存在返回0)
{
assert(pbf&&str);
//K个函数对于的地址都为1才能说明字符串存在,否则不存在
size_t len=strlen(str);
int i=0;
size_t index[FUNCMAXSIZE]={0};
for(i;infuncs;i++)
{
index[i]=pbf->funcsi%(pbf->asize);
if(Test(pbf,index[i])==0)
return 0;
}
return 1;
}

int Test(BloomFilter* pbf, size_t x) //判断所给数字是否存在(存在返回1,不存在返回0)
{
assert(pbf);
int index=x/8;//相当于/8
int num=x%8;
int ret=pbf->a[index]&(1<<num);
if (0==ret)
{
return 0;
}
else
return 1;

}

这两个函数的功能就是实现对数据的查找,同插入一样,我们需要先利用for循环依次调用哈希函数得到哈希值,随后取余找到相应的位,然后调用Test()函数,利用”与“位运算,如果为0,就表明不存在,如果为1,表明该位已为1。如果所有哈希函数对应的位都为1,则表明该数据存在,否则不存在。

哈希函数

由于哈希函数有限,不可能根据需要插入的数据个数,每次都能实现最优哈希函数个数,所以设置了哈希函数个数的上限为7

来自于常用的字符串哈希函数,七个哈希函数分别为BKDRHash,APHash,DJBHash,JSHash,RSHash,SDBMHash,BPHash

aJNWVg.png

运行测试

aJNubF.png

Cuckoo Filter

主要提出是为了解决不能删除的问题,并且提升了空间性能。它的哈希函数是成对的,每一个元素都是两个,分别映射到两个位置,一个是记录的位置,另一个是备用位置,这个备用位置是处理碰撞时用的,把原来占用位置的这个元素踢走,不过被踢出去的元素还有一个备用位置可以安置,如果备用位置上还有人,再把它踢走,如此往复。直到被踢的次数达到一个上限,才确认哈希表已满,并执行rehash操作

h2(x)不是随便选的,第一张表的备选位置是Hash(x),第二张表的备选位置是Hash(x)⊕hash(fingerprint(x)),即第一张表的位置与存储的指纹的Hash值做异或运算。这样可以直接用指纹的值 异或 原来位置的Hash值来计算出其另一张表的位置。

空的布谷鸟过滤器在连续插入2个相同元素后即会触发扩容,所以布谷鸟过滤器不允许插入超过(kb+1)个相同的元素,k 是指 hash 函数的个数 2,b是指单个位置上的座位数,如果要支持删除, 当插入重复数据的时候, 每次都需要存储一次指纹信息。这很容易造成插入的失败, 所以要限制重复数据的插入。但是布隆过滤器不能插入多个元素,因为对应的数组只会改变一次

优缺点

优点:

1.访问内存次数低

2.Hash函数计算简单

缺点:

1.内存空间不联系,CPU消耗大

2.容易出现装填循环问题

3.删除数据时,Hash冲突会引起误删

改进

改良的方案之一是增加 hash 函数, 让每个元素不止有两个巢, 这样可以大大降低碰撞的概率

方案二是给每个位置上挂多个巢, 这样不会马上就挤来挤去. 也能大大降低碰撞概率, 空间利用率虽然比第一种改良方案稍低,但cpu 缓存的利用率会提高不少.

Cuckoo Filter实现

结构体

typedef struct CuckooFilter //布谷鸟过滤器结构体
{ char* a;
size_t asize;//字节数
CuckooFunc *funcs;
int nfuncs;
}CuckooFilter;

与bloomfilter相似,只是cuckoofilter的结构体中a数组的大小和asize大小一致,而bloomfilter的a数组大小为asize的八分之一,因为一个数组可以存八个信息

初始化过滤器

void CuckooFilterInit(CuckooFilter* pbf,int m,CuckooFunc Func1,CuckooFunc Func2) //初始化布谷鸟过滤器
{
assert(pbf);
pbf->a = (char*)malloc(msizeof(char)); //开辟空间,每个位置存放指纹,1字节
pbf->funcs=(CuckooFunc
)malloc(2*sizeof(CuckooFunc)); //开辟空间,存放hash函数
memset(pbf->a,’\0’,m); //初始化为空
pbf->funcs[0]=Func1;
pbf->funcs[1]=Func2;
pbf->nfuncs=2;
pbf->asize=m;
}

相比于布隆过滤器,布谷鸟过滤器只需要两个hash函数,数量是固定的,省去了使用va_list(),va_start(),va_arg(),va_end()函数。开辟的空间每个位置只存放一个元素

插入数据

void CuckooFilterSet(CuckooFilter* pbf,char* str) //选取一个映射的地址,存入指纹值

{ assert(pbf&&str);
size_t len=strlen(str);
size_t index[2]={0};
char n;
char t;
size_t m=pbf->funcs0%256;
n=(char)m; //指纹值
t=&n;
index[0]=pbf->funcs1%(pbf->asize); //第一个hash地址
size_t p=pbf->funcs1%(pbf->asize);
index[1]=(index[0]^p)%(pbf->asize); //第一个哈希地址和指纹值取得的哈希地址进行或运算得到第二个hash地址
if(pbf->a[index[0]]’\0’) //第一个地址为空
pbf->a[index[0]]=n;
else if(pbf->a[index[1]]
’\0’) //第二个地址为空
pbf->a[index[1]]=n;
else //踢出原来的元素存入新元素
{ char q=pbf->a[index[0]];
str=&q;
pbf->a[index[0]]=n;
CuckooFilterchange(pbf,str,index[0],0); //被踢出的鸟前往另外的鸟巢
}
}
void CuckooFilterchange(CuckooFilter
pbf,char* str,size_t i,int num) //被踢出的鸟前往另外的鸟巢
{
if(num<=MAXNUM) //重复鸠占鹊巢操作次数小于允许的最大次数
{
size_t index[2]={0};
index[0]=i;
size_t p=pbf->funcs1%(pbf->asize);
index[1]=(index[0]^p)%(pbf->asize); //求出被踢出的元素的第二个巢
if(pbf->a[index[1]]==’\0’) //位置为空,放入元素结束操作
pbf->a[index[1]]=(str);
else //不为空,递归进行鸠占鹊巢
{ char q=pbf->a[index[1]];
pbf->a[index[1]]=(str);
str=&q;
CuckooFilterchange(pbf,str,index[1],num+1);
}
}
else
{ expand(pbf); //扩容操作
CuckooFilterchange(pbf,str,i,0); //扩容之后继续进行鸠占鹊巢
}
}
void expand(CuckooFilter
pbf) //扩容操作
{ assert(pbf);
//开辟空间
CuckooFilter
p;
CuckooFilter* q;
size_t m=pbf->asize;
p->a = (char*)malloc((m2)sizeof(char)); //布谷鸟过滤器空间扩为原来两倍
p->funcs=(CuckooFunc
)malloc(2
sizeof(CuckooFunc));
//初始化为0
memset(p->a,’\0’,(m2));
p->funcs[0]=pbf->funcs[0];
p->funcs[1]=pbf->funcs[1];
p->nfuncs=2;
p->asize=(m
2);
for(int i=0;i<m;i++) //将原过滤器中的内容全部复制
p->a[i]=pbf->a[i];
q=pbf;
pbf=p;
CuckooFilterDestroy(q); //销毁原过滤器
}

由于布谷鸟过滤器每个位置只存一个数据的指纹,而且每个数据指纹位置只可能是两个,所以存在争抢的问题,即不同数据的hash值一致。若两个位置都不为空,就需要剔出原来的元素,存入新的元素,而被踢出的元素只能重新存入它另外的位置,如果另外的位置又有其他元素,则踢出那个元素后存入该元素,如此往复直到位置为空,插入元素之后结束。但是重复的鸠占鹊巢操作有一个上限次数,超过了这个次数就要进行扩容,扩容之后重新进行插入。

两个hash函数,第一个hash函数用来求指纹值,第二个hash函数用来求所在的位置。第一个位置直接用输入的数据进行hash即可,而第二个位置是现在的位置异或它指纹的哈希值(异或运算的特性baa=b)

删除查找数据

int CuckooFilterDelete(CuckooFilter* pbf,char* str) //删除元素
{
assert(pbf&&str);
size_t len=strlen(str);
size_t index[2]={0};
char n;
char *t;
size_t m=pbf->funcs0%256;
n=(char)m;
t=&n;
index[0]=pbf->funcs1%(pbf->asize);
size_t p=pbf->funcs1%(pbf->asize);
index[1]=(index[0]^p)%(pbf->asize);
if(pbf->a[index[0]]==n)
{ pbf->a[index[0]]=’\0’;
return 1;
}
else if(pbf->a[index[1]]==n)
{ pbf->a[index[1]]=’\0’;
return 1;
}
return 0;
}

int CuckooFilterTest(CuckooFilter* pbf,char* str) //查找元素,有一个查找成功即存在,两个位置都不成功则表明不存在
{ assert(pbf&&str);
size_t len=strlen(str);
size_t index[2]={0};
char n;
char *t;
size_t m=pbf->funcs0%256;
n=(char)m;
t=&n;
index[0]=pbf->funcs1%(pbf->asize);
size_t p=pbf->funcs1%(pbf->asize);
index[1]=(index[0]^p)%(pbf->asize);
if(pbf->a[index[0]]==n)
return 1;
else if(pbf->a[index[1]]==n)
return 1;
else
return 0;

}

删除相对于插入简单许多,只需要先求出该数据指纹可能存在的两个位置,然后找到其中所在的一个位置清空即可,如果都不存在,说明该数据未插入其中,无需删除。而查找数据的过程与删除几乎一致,即在对应位置找到了指纹值说明该数据存在,否则说明不存在

测试结果

aY8fDe.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值