前言
上一篇博客提到了位图,位图是十分高效的数据结构,但可惜的是只支持整型,今天这篇博客的主角是布隆过滤器,他与位图有异曲同工之妙。(不了解位图可以点击下面这篇博客快速了解)位图(bitset)--明确场景极致性能-CSDN博客)
布隆过滤器定义
布隆过滤器与位图类似,都是对比特位进行操作,不同的是位图只对一个位置标记,但布隆过滤器可以有多个哈希函数,对多个位置标记。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
如下图所示,当我们插入一个数的时候,把多个位置标记位1
当我们判断当前元素在不在时,就可以依次判断是否为1即可,假如3个位置有一个为0,那么该元素一定不在,假如3个位置都为1,那么我们不能确定该元素一定在,只能时可能在。因为当前hash位置1可能是由于插入其他元素造成的,即发生哈希冲突。
所以这个数据结构就和他的名字一样,起到了过滤器的作用,将部分滥竽充数的筛出来。
布隆过滤器使用
位图可以标记某个整数是否存在,除非两个数相同,否则不可能出现哈希冲突。但当我们想用其他类型的变量建立映射关系时一定会发生哈希碰撞造成误判,此时我们在查询当前元素是否存在就会产生误差,如果返回不存在,那么该元素一定不存在,如果返回存在则是可能存在。
在一些对精准度要求不严格的地方,就可以用布隆过滤器,布隆过滤器的误差律可以通过数学推导求出,然后根据数学公式可以尽可能的减小误判率。对数学过程感兴趣可以看下面博客。
布隆过滤器概念及其公式推导_布隆过滤器公式推导-CSDN博客
在这里就直接提供最终结论了。
我们可以直接理解,当哈希函数一定时,布隆过滤器越长冲突越少,插入越少元素,冲突越少。
布隆过滤器实现
哈希函数
布隆过滤器一般用于字符串查找,那么我们就可以给模板默认string类型。关于字符串的Hash函数有很多种这里列举三个用的较多的Hash函数。
struct HashFuncBKDR
{
// @detail 本 算法由于在Brian Kernighan与Dennis Ritchie的《The CProgramming Language》
// 一书被展示而得 名,是一种简单快捷的hash算法,也是Java目前采用的字符串的Hash算法累乘因子为31。
size_t operator()(const std::string& s)
{
size_t hash = 0;
for (auto ch : s)
{
hash *= 31;
hash += ch;
}
return hash;
}
};
struct HashFuncAP
{
// 由Arash Partow发明的一种hash算法。
size_t operator()(const std::string& s)
{
size_t hash = 0;
for (size_t i = 0; i < s.size(); i++)
{
if ((i & 1) == 0) // 偶数位字符
{
hash ^= ((hash << 7) ^ (s[i]) ^ (hash >> 3));
}
else // 奇数位字符
{
hash ^= (~((hash << 11) ^ (s[i]) ^ (hash >> 5)));
}
}
return hash;
}
};
struct HashFuncDJB
{
// 由Daniel J. Bernstein教授发明的一种hash算法。
size_t operator()(const std::string& s)
{
size_t hash = 5381;
for (auto ch : s)
{
hash = hash * 33 ^ ch;
}
return hash;
}
};
类模板
布隆过滤器实现也比较简单,无非就是设置三次位图,一般的布隆过滤器不支持删除,只能重建。
//N 预先开辟个数 一个数分配X位
template<size_t N,
size_t X = 5,
class K = std::string,
class Hash1 = HashFuncBKDR,
class Hash2 = HashFuncAP,
class Hash3 = HashFuncDJB>
class BloomFilter
{
public:
void set(const K& key)
{
Hash1 h1;
Hash2 h2;
Hash3 h3;
int hs1 = h1(key)%M;
int hs2 = h2(key)%M;
int hs3 = h3(key)%M;
//设置三次
_bs.set(hs1);
_bs.set(hs2);
_bs.set(hs3);
}
//检测是否在,有误判
bool test(const K& key)
{
Hash1 h1;
Hash2 h2;
Hash3 h3;
int hs1 = h1(key) % M;
if (!_bs.test(hs1))
return false;
int hs2 = h2(key) % M;
if (!_bs.test(hs2))
return false;
int hs3 = h3(key) % M;
if (!_bs.test(hs3))
return false;
return true;
}
private:
static const size_t M = N * X;
bitset<M> _bs;
};
上述代码就实现了布隆过滤器的核心操作,总体难度是不大的。
测试
void TestBloomFilter1()
{
srand(time(0));
const size_t N = 1000000;
BloomFilter<N> bf;
//BloomFilter<N, 3> bf;
//BloomFilter<N, 10> bf;
std::vector<std::string> v1;
//std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
//std::string url = "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=65081411_1_oem_dg&wd=ln2&fenlei=256&rsv_pq=0x8d9962630072789f&rsv_t=ceda1rulSdBxDLjBdX4484KaopD%2BzBFgV1uZn4271RV0PonRFJm0i5xAJ%2FDo&rqlang=en&rsv_enter=1&rsv_dl=ib&rsv_sug3=3&rsv_sug1=2&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&inputT=330&rsv_sug4=2535";
std::string url = "猪八戒";
for (size_t i = 0; i < N; ++i)
{
v1.push_back(url + std::to_string(i));
}
for (auto& str : v1)
{
bf.set(str);
}
// v2跟v1是相似字符串集(前缀一样),但是后缀不一样
v1.clear();
for (size_t i = 0; i < N; ++i)
{
std::string urlstr = url;
urlstr += std::to_string(9999999 + i);
v1.push_back(urlstr);
}
size_t n2 = 0;
for (auto& str : v1)
{
if (bf.Test(str)) // 误判
{
++n2;
}
}
cout << "相似字符串误判率:" << (double)n2 / (double)N << endl;
// 不相似字符串集 前缀后缀都不一样
v1.clear();
for (size_t i = 0; i < N; ++i)
{
//string url = "zhihu.com";
string url = "孙悟空";
url += std::to_string(i + rand());
v1.push_back(url);
}
size_t n3 = 0;
for (auto& str : v1)
{
if (bf.test(str))
{
++n3;
}
}
cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;
}
由此我们便可以测出误判率。在10万数据下误判率在10%以下,
在100万数据下误判率在10%左右,
源码
头文件
#pragma once
#include<bitset>
#include<iostream>
using namespace std;
struct HashFuncBKDR
{
// @detail 本 算法由于在Brian Kernighan与Dennis Ritchie的《The CProgramming Language》
// 一书被展示而得 名,是一种简单快捷的hash算法,也是Java目前采用的字符串的Hash算法累乘因子为31。
size_t operator()(const std::string& s)
{
size_t hash = 0;
for (auto ch : s)
{
hash *= 31;
hash += ch;
}
return hash;
}
};
struct HashFuncAP
{
// 由Arash Partow发明的一种hash算法。
size_t operator()(const std::string& s)
{
size_t hash = 0;
for (size_t i = 0; i < s.size(); i++)
{
if ((i & 1) == 0) // 偶数位字符
{
hash ^= ((hash << 7) ^ (s[i]) ^ (hash >> 3));
}
else // 奇数位字符
{
hash ^= (~((hash << 11) ^ (s[i]) ^ (hash >> 5)));
}
}
return hash;
}
};
struct HashFuncDJB
{
// 由Daniel J. Bernstein教授发明的一种hash算法。
size_t operator()(const std::string& s)
{
size_t hash = 5381;
for (auto ch : s)
{
hash = hash * 33 ^ ch;
}
return hash;
}
};
//N 预先开辟个数 一个数分配X位
template<size_t N,
size_t X = 5,
class K = std::string,
class Hash1 = HashFuncBKDR,
class Hash2 = HashFuncAP,
class Hash3 = HashFuncDJB>
class BloomFilter
{
public:
void set(const K& key)
{
Hash1 h1;
Hash2 h2;
Hash3 h3;
int hs1 = h1(key)%M;
int hs2 = h2(key)%M;
int hs3 = h3(key)%M;
//设置三次
_bs.set(hs1);
_bs.set(hs2);
_bs.set(hs3);
}
//检测是否在,有误判
bool test(const K& key)
{
Hash1 h1;
Hash2 h2;
Hash3 h3;
int hs1 = h1(key) % M;
if (!_bs.test(hs1))
return false;
int hs2 = h2(key) % M;
if (!_bs.test(hs2))
return false;
int hs3 = h3(key) % M;
if (!_bs.test(hs3))
return false;
return true;
}
private:
static const size_t M = N * X;
bitset<M> _bs;
};
源文件
#include"BloomFilter.h"
#include<vector>
#include<string>
void TestBloomFilter1()
{
srand(time(0));
const size_t N = 100;
BloomFilter<N> bf;
//BloomFilter<N, 3> bf;
//BloomFilter<N, 10> bf;
std::vector<std::string> v1;
//std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
//std::string url = "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=65081411_1_oem_dg&wd=ln2&fenlei=256&rsv_pq=0x8d9962630072789f&rsv_t=ceda1rulSdBxDLjBdX4484KaopD%2BzBFgV1uZn4271RV0PonRFJm0i5xAJ%2FDo&rqlang=en&rsv_enter=1&rsv_dl=ib&rsv_sug3=3&rsv_sug1=2&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&inputT=330&rsv_sug4=2535";
std::string url = "猪八戒";
for (size_t i = 0; i < N; ++i)
{
v1.push_back(url + std::to_string(i));
}
for (auto& str : v1)
{
bf.set(str);
}
// v2跟v1是相似字符串集(前缀一样),但是后缀不一样
v1.clear();
for (size_t i = 0; i < N; ++i)
{
std::string urlstr = url;
urlstr += std::to_string(9999999 + i);
v1.push_back(urlstr);
}
size_t n2 = 0;
for (auto& str : v1)
{
if (bf.test(str)) // 误判
{
++n2;
}
}
cout << "相似字符串误判率:" << (double)n2 / (double)N << endl;
// 不相似字符串集 前缀后缀都不一样
v1.clear();
for (size_t i = 0; i < N; ++i)
{
//string url = "zhihu.com";
string url = "孙悟空";
url += std::to_string(i + rand());
v1.push_back(url);
}
size_t n3 = 0;
for (auto& str : v1)
{
if (bf.test(str))
{
++n3;
}
}
cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;
}
int main()
{
TestBloomFilter1();
return 0;
}