在某些特定场景下,当我们需要处理海量数据(如URL、邮件地址、用户名等非整数数据)并快速判断其是否存在时,传统的位图(BitMap)因其只能处理整数类型而无法适用;而红黑树或哈希表等数据结构虽然可以处理各种数据类型,但当数据量达到亿级时,内存消耗将变得不可接受。这时,布隆过滤器(Bloom Filter)便成为了一个理想的解决方案。
布隆过滤器是由计算机科学家Burton Howard Bloom在1970年发表的一篇名为《Space/Time Trade-offs in Hash Coding with Allowable Errors》的论文中首次提出的。它是一种空间效率极高的概率型数据结构,具有以下显著特点:
布隆过滤器的核心实现包含三个关键组件:
其工作流程如下:
布隆过滤器的精妙之处在于:
布隆过滤器以其独特的空间效率和概率性特征,在大数据处理领域占据着不可替代的地位,是典型的以准确率换取空间效率的算法设计典范。

说明:本推导涉及概率论、极限、对数运算和求导等数学知识。数学基础较好的读者可以深入理解,其他读者记住结论即可。
单个哈希函数将某位设置为1的概率:1/m
单个哈希函数不将某位置为1的概率:1 - 1/m
经过k次哈希后,某位仍不为1的概率:(1 - 1/m)^k
根据极限公式:
lim(m→∞) (1 - 1/m)^m = e^-1
(1 - 1/m)^k ≈ e^(-k/m)插入n个元素后,某位不置为1的概率: (1 - 1/m)^(kn) ≈ e^(-kn/m)
插入n个元素后,某位置为1的概率: 1 - (1 - 1/m)^(kn) ≈ 1 - e^(-kn/m)
查询误判概率(所有k次哈希都命中1的概率): [1 - (1 - 1/m)^(kn)]^k ≈ (1 - e^(-kn/m))^k
布隆过滤器的误判率为:
f(k) = (1 - e^(-kn/m))^k
= (1 - (1/e)^(kn/m))^k当k固定时:
优化哈希函数数量k: 当m和n固定时,取k = (m/n)ln2可使误判率最低
计算位数组长度m: 给定期望误判率p和插入元素数n:
m = -(n*ln p)/(ln 2)^2以上两个公式的推导过程尤其复杂,这里放两篇博客链接布隆过滤器(Bloom Filter)- 原理、实现和推导_ 布隆过滤器原理-CSDN博客 和[布隆过滤器BloomFilter] 举例说明+证明推导_bloom filter 最佳hash函 数数量 推导-CSDN博客
size_t operator()(const std::string& s) {
size_t hash = 0;
for (auto ch : s) {
hash *= 31; // 乘法因子
hash += ch; // 累加字符值
}
return 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;
}size_t operator()(const std::string& s) {
size_t hash = 5381; // 魔术初始值
for (auto ch : s) {
hash = hash * 33 ^ ch; // 乘33后异或
}
return hash;
}template<size_t N, // 预期元素数量
size_t X = 5, // 每个元素比特数
class K = string, // 元素类型
class Hash1 = HashFuncBKDR, // 哈希函数1
class Hash2 = HashFuncAP, // 哈希函数2
class Hash3 = HashFuncDJB> // 哈希函数3private:
static const size_t M = N * X; // 位图大小
RO::bitset<M> _bt; // 底层位图M = N × X 比特
void Set(const K& key) {
size_t hash1 = Hash1()(key) % M;
size_t hash2 = Hash2()(key) % M;
size_t hash3 = Hash3()(key) % M;
_bt.set(hash1);
_bt.set(hash2);
_bt.set(hash3);
}bool Test(const K& key) {
size_t hash1 = Hash1()(key) % M;
if (!_bt.test(hash1)) return false; // 肯定不存在
size_t hash2 = Hash2()(key) % M;
if (!_bt.test(hash2)) return false; // 肯定不存在
size_t hash3 = Hash3()(key) % M;
if (!_bt.test(hash3)) return false; // 肯定不存在
return true; // 可能存在(可能有误判)
}X 值 | 位图大小 | 误判率 (n=N) | 100万元素内存占用 | 特点 |
|---|---|---|---|---|
3 | 3N | ≈ 5% | 375 KB | 内存最小,误判率高 |
5(默认) | 5N | ≈ 2% | 625 KB | 良好平衡 |
8 | 8N | ≈ 0.5% | 1 MB | 低误判率 |
10 | 10N | ≈ 0.1% | 1.25 MB | 极低误判率 |
16 | 16N | ≈ 0.01% | 2 MB | 接近完美但内存大 |
经验平衡点:
哈希函数数量匹配:
当使用 k 个哈希函数时,最优 X ≈ k/ln(2)×(-ln(p))
对于 p=0.02(2%误判率)和 k=3:
X ≈ 3 / (0.693) × (-ln(0.02)) ≈ 5.14因此 X=5 接近理论最优值
// 高精度场景(如金融)
BloomFilter<100000, 10> highPrecisionFilter;
// 内存敏感场景(如IoT设备)
BloomFilter<5000, 3> lowMemoryFilter;
// 通用场景(默认)
BloomFilter<1000000> defaultFilter; // 使用X=5最优 k = (M/n) * ln(2) ≈ 0.7 * (M/n)X 参数是布隆过滤器设计的关键权衡因子:
通过合理选择 X 值,可以在可接受的内存消耗下,获得满足业务需求的误判率水平,这正是布隆过滤器在大数据处理中如此强大的原因。
先来简单测试一下函数功能:
void TestBloomFilter1()
{
BloomFilter<10> bf;
bf.Set("猪八戒");
bf.Set("孙悟空");
bf.Set("唐僧");
cout << bf.Test("猪八戒") << endl;
cout << bf.Test("孙悟空") << endl;
cout << bf.Test("唐僧") << endl;
cout << bf.Test("沙僧") << endl;
cout << bf.Test("猪八戒1") << endl;
cout << bf.Test("猪戒八") << endl;
}运行结果:

测试计算误判率:
这里我们再设计一个公式计算误判率的函数来作比较:
// 获取公式计算出的误判率
double getFalseProbability()
{
double p = pow((1.0 - pow(2.71, -3.0 / X)), 3.0);
return p;
}void TestBloomFilter2()
{
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;
cout << "公式计算出的误判率:" << bf.getFalseProbability() << endl;
}运行结果:

布隆过滤器(Bloom Filter)作为一种高效的空间概率型数据结构,其默认实现不支持删除操作。这是由于布隆过滤器的核心机制决定的:

为解决删除问题,研究者提出了计数布隆过滤器(Counting Bloom Filter):
计数布隆过滤器并非完美解决方案,存在以下问题:
这是一个典型的Top K问题,可以通过堆结构高效解决。具体实现方法在我们数据结构"堆"章节中已有详细讲解。TOP-K
这些题目在我们讲解位图数据结构的章节中均有涉及。位图
优势:
局限:
使用哈希函数将大文件划分为小文件对,确保:

选择哈希函数:
确定切分份数:
切分份数 = 总文件大小 / 目标小文件大小
切分过程:
文件A切分:
query → 哈希函数 → hash_value % 1000 → 写入A_i文件 (i=0..999)
文件B切分:
query → 相同哈希函数 → hash_value % 1000 → 写入B_i文件处理流程:
for i in range(0, 999):
加载小文件A_i到内存哈希表
遍历小文件B_i的每个query:
if query in 哈希表:
输出到结果文件内存控制:
当小文件过大时(内存不足):
原因分析: a) 大量重复query → 使用哈希表自动去重 b) 大量不同query(哈希冲突)→ 需要二次切分
二次切分策略:
处理流程:
try:
加载小文件A_i到内存
catch 内存不足异常:
选择新哈希函数H2
将A_i切分为A_i0, A_i1... A_i99 (100份)
将B_i切分为B_i0, B_i1... B_i99
for j in range(0,99):
递归处理(A_ij, B_ij)文件对切分份数 = max(文件大小/(内存×0.6), 100)
特性 | 布隆过滤器法 | 哈希切分法 |
|---|---|---|
准确性 | 近似(有假阳性) | 精确 |
内存需求 | 中等(需分块) | 低(自适应) |
磁盘I/O | 中高(多次扫描) | 中等(两次主要I/O) |
实现复杂度 | 简单 | 中等 |
处理时间 | 较短 | 较长 |
最佳适用场景 | 初步筛选/去重 | 精确分析/审计 |
选择建议:
在1GB内存限制下处理100亿query文件的交集问题:
该方案不仅适用于query交集问题,还可扩展至海量数据去重、相似度计算、关联分析等多种场景,是大数据处理的核心技术之一。
解题思路与上题类似:
核心原理:

#pragma once
#include <string>
#include <iostream>
#include "bitset.h"
namespace RO
{
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;
}
};
template<size_t N,
size_t X = 5,
class K = string,
class Hash1 = HashFuncBKDR,
class Hash2 = HashFuncAP,
class Hash3 = HashFuncDJB>
class BloomFilter
{
public:
void Set(const K& key)
{
size_t hash1 = Hash1()(key) % M;
size_t hash2 = Hash2()(key) % M;
size_t hash3 = Hash3()(key) % M;
_bt.set(hash1);
_bt.set(hash2);
_bt.set(hash3);
}
bool Test(const K& key)
{
size_t hash1 = Hash1()(key) % M;
if (!_bt.test(hash1)) return false;
size_t hash2 = Hash2()(key) % M;
if (!_bt.test(hash2)) return false;
size_t hash3 = Hash3()(key) % M;
if (!_bt.test(hash3)) return false;
return true; // 可能存在误判
}
// 获取公式计算出的误判率
double getFalseProbability()
{
double p = pow((1.0 - pow(2.71, -3.0 / X)), 3.0);
return p;
}
private:
static const size_t M = N * X;
RO::bitset<M> _bt;
};
void TestBloomFilter1()
{
BloomFilter<10> bf;
bf.Set("猪八戒");
bf.Set("孙悟空");
bf.Set("唐僧");
cout << bf.Test("猪八戒") << endl;
cout << bf.Test("孙悟空") << endl;
cout << bf.Test("唐僧") << endl;
cout << bf.Test("沙僧") << endl;
cout << bf.Test("猪八戒1") << endl;
cout << bf.Test("猪戒八") << endl;
}
void TestBloomFilter2()
{
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;
cout << "公式计算出的误判率:" << bf.getFalseProbability() << endl;
}
}