布隆过滤器
布隆过滤器(Bloom Filter)是一个由布隆在1970年提出的概率型数据结构,它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器的主要特点是高效的插入和查询,可以用于检索一个元素是否在一个集合中。
原理:
- 布隆过滤器是采用位图的方式来记录数据的存在与否
- 将要存储的数据同时映射到位图的多个节点上
- 当寻找数据时,只需将要找的数据进行映射,观察全部映射节点的情况,就可以判断数据存在与否
其核心功能如下(此处以数据库使用布隆过滤器为例):
解释:
对于一个数据的查找,会先去布隆过滤器中进行多个映射点的查询。会有如下情况发生:
- 只有当每个映射点都存在(对应位置为1)的时候,才能说明该数据可能存在,下一步就去Database中进一步确认(因为冲突的原因,所以可能某个数据的每个映射点虽然都为1,但是该数据并不存在其中)。
- 如果有任何一个映射点不存在(对应位置为0)的时候,就说明该数据并不在存储空间内。
如何选择映射
布隆的核心就是最大限度减少冲突问题,进而在高查找效率的情况下,减少寻找错误的可能性!
现在提出一个问题:如果我只采用一个映射(每个数据只映射到一个位图节点上),由于哈希的局限性以及开辟空间的有限性,就会出现多个数据映射到同一个位置上
所以布隆过滤器就采用了多映射的方案(即一个值映射到多个位图节点上),大大缩小了冲突的可能性
优点:
- 多于不同的数据,他们在某个哈希函数下可能映射到了同一个位置,但是对于多个映射全相同的概率极小,这就使得哈希冲突变小
- 由于数据的查询一般采用轮询的方式(或者树形结构)导致时间复杂度较大,而布隆过滤器的查询效率是大O(1)(忽略极个别的全冲突情况)
缺点:
- 不存在漏报,但存在误报(即对于数据存在的判断有失准确性)。但严格来说该缺点不算大事
- 不能在布隆过滤器中删除元素,因为这会影响到别的与该位图节点有关联的数据(但可以改造他,使他可以实现删除操作)
哈希函数的选择:
可以采用BKDR
、AP
等哈希函数
选择的哈希函数的前提是:需要保证映射后的值能合理放到位图中的对应位置上(如果你映射出来一个:164gxap3,那么你该放入位图中的哪个位置呢?是吧)
应用场景
- 快速检索元素:布隆过滤器能够快速地检索一个元素是否在一个集合中,而无需访问整个集合。这大大提高了查询效率,尤其是在处理大数据集时。
- 网页URL去重:在搜索引擎或爬虫系统中,布隆过滤器可以用于过滤已经访问过的网页URL,避免重复访问和存储。
- 垃圾邮件过滤:布隆过滤器可以存储已知的垃圾邮件发送者的地址或特征,当接收到新邮件时,可以通过检查邮件地址或内容是否在布隆过滤器中来判断其是否为垃圾邮件。
- 数据库查询优化:在数据库中,布隆过滤器可以用于减少不必要的磁盘查找操作。例如,当查询一个不存在的行或列时,布隆过滤器可以快速判断该数据是否存在于数据库中,从而避免不必要的磁盘访问。
- 恶意代码检测:在网络安全领域,布隆过滤器可以用于检测恶意代码的僵尸网络或分析不断变化的市场数据。通过存储已知的恶意代码特征或行为模式,布隆过滤器可以快速判断新发现的代码是否为恶意代码。
- 生物信息学应用:布隆过滤器可以用于快速查找DNA测序数据中的基因序列,以及在其他生物学和遗传学领域如蛋白质组学、转录组学和基因组学等进行数据分析。
- 非结构化大数据分析:在企业数据分析中,布隆过滤器可以帮助企业快速检测网站中的指定元素(如URL中的关键字、用户搜索的关键字等),从而找出其中的趋势和模式,帮助企业更好地投资和发展。
- 机器学习应用:在机器学习领域,布隆过滤器可以用于快速处理海量数据,提取特征并构建模型。由于布隆过滤器的快速查询能力,它可以大大提高模型训练和预测的效率。
代码实现以及成果演示
存储的元素:“hello”, “world”, “你好”, “no thank”, “why”, “4”, “-78”
查找的元素:“world”,“你好”,“早上好”,“no thank”,“8”,“4”,“0”,“-78”
分别采用了BKDR、AP、DJB三个哈希函数
uint64_t BKDR_Hash(const char* str) {uint64_t seed = 131; // 31 131 1313 13131 131313 etc.. uint64_t hash = 0;while (*str) {hash = hash * seed + (*str++);}return hash;
}
uint64_t AP_Hash(const char* str)
{uint64_t hash = 0;int i;for (i = 0; *str; i++){if ((i & 1) == 0){hash ^= ((hash << 7) ^ (*str++) ^ (hash >> 3));}else{hash ^= (~((hash << 11) ^ (*str++) ^ (hash >> 5)));}}return (hash & 0x7FFFFFFF);
}
uint64_t DJB_Hash(const char* str)
{uint64_t hash = 5381;while (*str){hash += (hash << 5) + (*str++);}return (hash & 0x7FFFFFFF);
}
int main()
{bitset<10000> st;vector<string> vstr{"hello", "world", "你好", "no thank", "why", "4", "-78"};for (int i = 0; i < vstr.size(); i++){st.set(BKDR_Hash(vstr[i].c_str()) % st.size());st.set(AP_Hash(vstr[i].c_str()) % st.size());st.set(DJB_Hash(vstr[i].c_str()) % st.size());}vector<string> vfind{"world","你好","早上好","no thank","8","4","0","-78"};for (int i = 0; i < vfind.size(); i++){cout << vfind[i];if (st[BKDR_Hash(vfind[i].c_str()) % st.size()]&& st[AP_Hash(vfind[i].c_str()) % st.size()]&& st[DJB_Hash(vfind[i].c_str()) % st.size()]){cout << " exist" << endl;}else{cout << " not exixt" << endl;}}return 0;
}
查找结果:
world exist
你好 exist
早上好 not exixt
no thank exist
8 not exixt
4 exist
0 not exixt
-78 exist
优化成可实现删除操作
对于前面的布隆过滤器的写法,有一个很明显的问题:对于数据的删除是不可实现的,因为对任意一个数据映射位置的删除,由于哈希冲突,都可能导致影响到别的数据的映射关系
所以我们采用引用计数+扩展位图的方式来实现删除操作,具体操作如下:
采用引用计数的方案,对每一个哈希映射值都采用引用计数。每新增一个映射关系就++,减少一个映射关系就- -
如此一来,对位图也需要有所更改:增大位图空间开销,比如用8个bit表示一个映射关系,而在这8个bit中就可以用引用计数实现0-255的可映射数量