文章目录
- 一、位图
-
- [1.1 位图概念引入](#1.1 位图概念引入)
- [1.2 位图设计与实现](#1.2 位图设计与实现)
- 二、布隆过滤器
-
- [2.1 概念简介](#2.1 概念简介)
- [2.2 布隆过滤器误判率推导](#2.2 布隆过滤器误判率推导)
- [2.3 布隆过滤器的实现](#2.3 布隆过滤器的实现)
- [2.4 布隆过滤器应用](#2.4 布隆过滤器应用)
- 三、海量数据处理问题
-
- [3.1 如何在10亿个整数⾥⾯求最⼤的前100个?](#3.1 如何在10亿个整数⾥⾯求最⼤的前100个?)
- [3.2 给两个⽂件,分别有100亿个query,只有1G内存,如何找到两个⽂件交集?](#3.2 给两个⽂件,分别有100亿个query,只有1G内存,如何找到两个⽂件交集?)
- [3.3 给⼀个超过100G⼤⼩的log file,log中存着ip地址,设计算法找到出现次数最多的ip地址?如何查找出现次数前10的ip地址?](#3.3 给⼀个超过100G⼤⼩的log file,log中存着ip地址,设计算法找到出现次数最多的ip地址?如何查找出现次数前10的ip地址?)
- 四、测试源码
-
- [4.1 位图测试](#4.1 位图测试)
- [4.2 布隆过滤器测试](#4.2 布隆过滤器测试)
一、位图
1.1 位图概念引入
为引入位图,请分析下面问题并提出解决方案:
给40亿个不重复的⽆符号整数,没排过序。给⼀个⽆符号整数,如何快速判断⼀个数是否在这40亿个数中。(本题为腾讯/百度等公司出过的⾯试题)
- 解题思路1:暴⼒遍历,时间复杂度 O ( N ) O(N) O(N),太慢
- 解题思路2:排序+⼆分查找。时间复杂度消耗 O ( N ∗ l o g N ) + O ( l o g N ) O(N*logN)+O(logN) O(N∗logN)+O(logN)
思路2的深⼊分析:解题思路2是否可⾏,我们先算算 40 40 40亿个数据⼤概需要多少内存?
- 1 G = 1024 M B = 1024 ∗ 1024 K B = 1024 ∗ 1024 ∗ 1024 B y t e 1G=1024MB=1024*1024KB=1024*1024*1024Byte 1G=1024MB=1024∗1024KB=1024∗1024∗1024Byte
约等于 10 10 10亿多 B y t e Byte Byte
那么 40 40 40亿个整数约等于 16 G 16G 16G,说明 40 40 40亿个数是⽆法直接放到内存中的,只能放到硬盘⽂件中。⽽⼆分查找只能对内存数组中的有序数据进⾏查找。
- 解题思路3:数据是否在给定的整型数据中,结果是在或者不在,刚好是两种状态,那么可以使⽤⼀个⼆进制⽐特位来代表数据是否存在的信息,如果⼆进制⽐特位为1,代表存在,为0代表不存在。那么我们设计⼀个⽤位表示数据是否存在的数据结构,这个数据结构就叫位图。
位图是一种用二进制位(0或1)表示数据是否存在的高效存储结构,常用于快速查找、去重和状态标记,具有节省空间和访问速度快的特点。
应用示例:
实现一个算法,确定一个字符串 s 的所有字符是否全部不同。 示例 1:
- 输入:s = "leetcode"
- 输出:false
示例 2:
- 输入:s = "abc"
- 输出:true
限制:
- 0 <= len(s) <= 100
- s[i] 仅包含小写字母
- 如果你不使用额外的数据结构,会很加分。
cppbool isUnique(string astr) { int bit = 0; for(int i=0;i<astr.size();i++) { if(bit>>(astr[i]-'a')&1) return false; else bit=bit|(1<<(astr[i]-'a')); } return true; } }; ```
题目链接:面试题 01.01. 判定字符是否唯一
1.2 位图设计与实现
位图本质是一张直接定址法的哈希表,一个int类型占4个字节,可以提供32个bit位。也就可以记录32个数据的存在状态。

位图简单实现:
cpp//构造函数 BitMap(size_t num) { //num除以32计算需要的多少个4字节空间 _bits.resize((num>>5)+1,0); } //把数据标为1 void set(size_t val) { int index = val/32; int pos = val%32; _bits[index]|=(1<<pos); } //把数据标记为0 void erase(size_t val) { int index = val/32; int pos = val%32; _bits[index]&=(~(1<<pos)); } //查看数据是否存在 bool find(size_t val) { int index = val/32; int pos = val%32; return (_bits[index]>>pos)&1; } private: std::vector<int> _bits; };
实现中需要注意的是,C/C++没有对应位的类型,只能看int/char这样整类型,我们再通过位运算去控制对应的⽐特位。⽐如我们数据存到vector<int>中,相当于每个int值映射对应的32个值,⽐如第⼀个整型映射 0 − 31 0-31 0−31对应的位,第⼆个整型映射32-63对应的位,后⾯的以此类推,那么来了⼀个整型值 x x x, i = x / 32 i=x/32 i=x/32; j = x % 32 j=x\%32 j=x%32;计算出 x x x映射的值在vector的第i个整型数据的第j位。
解决给40亿个不重复的⽆符号整数,查找⼀个数据的问题,我们要给位图开 2 32 2^{32} 232个位,注意不能开40亿个位,因为映射是按⼤⼩映射的,我们要按数据⼤⼩范围开空间,范围是是 0 0 0到 2 32 − 1 2^{32}-1 232−1,所以需要开 2 32 2^{32} 232个位。然后从⽂件中依次读取每个set到位图中,然后就可以快速判断⼀个值是否在这40亿个数中了。
位图优缺点:
- 优点:增删查改快,节省空间
- 缺点:只适⽤于整型
C++库中的位图:
二、布隆过滤器
2.1 概念简介
在一些场景下,大量数据需要判断是否存在,而这些数据不是整型,那么位图就不能使用了,使用红黑树/哈希表等结构,内存空间可能不够。那么这些场景就需要布隆过滤器了。
布隆过滤器的思想是将非整型数据转化为整型,再做位图映射。是由布隆(Burton Howard Bloom)在1970年提出的⼀种紧凑型的、⽐较巧妙的概率型数据结构,特点是⾼效地插⼊和查询,可以⽤来告诉你"某样东西⼀定不存在或者可能存在",它是⽤多个哈希函数,将⼀个数据映射到位图结构中。此种⽅式不仅可以提升查询效率,也可以节省⼤量的内存空间。
不同的key可以会转化成相同的整型值,这样就会存在冲突,所以哈希函数的设计直接影响了冲突率的大小,而通过多个哈希函数映射多个位可以有效的降低冲突率。(注:冲突无法被解决,只能降低冲突率)
所以判断⼀个值key存在是不准确的,判断⼀个值key不存在是准确的。
如下:

-
注意1:哈希函数并不是越多越好,哈希函数太多会让更多的位设为1,反而更加冲突。
-
注意2:误判------目标不在,但全是1才是误判。
-
注意3:布隆过滤器本身不支持删除,因为可能会影响其他值的映射。
布隆过滤器可以使用引用计数来解决删除问题,删除一个值引用计数减1,直到引用计数为0然后将相应bit位置0,但引入了新的复杂性和开销,这也恰恰"违背"了原始布隆过滤器最纯粹的设计目标。
2.2 布隆过滤器误判率推导
假设:
- m m m:布隆过滤器的 bit 长度
- n n n:插入过滤器的元素个数
- k k k:哈希函数的个数
布隆过滤器哈希函数等条件下某个位设置为 1 的概率:
1 m \frac{1}{m} m1
布隆过滤器哈希函数等条件下某个位设置不为 1 的概率:
1 − 1 m 1 - \frac{1}{m} 1−m1
在经过 k k k 次哈希后,某个位依旧不为 1 的概率:
( 1 − 1 m ) k \left(1 - \frac{1}{m}\right)^k (1−m1)k
根据极限公式:
lim m → ∞ ( 1 − 1 m ) m = e \lim_{m \to \infty} \left(1 - \frac{1}{m}\right)^m = e m→∞lim(1−m1)m=e
( 1 − 1 m ) k = ( ( 1 − 1 m ) m ) k m ≈ e − k m \left(1 - \frac{1}{m}\right)^k = \left(\left(1 - \frac{1}{m}\right)^m\right)^{\frac{k}{m}} \approx e^{-\frac{k}{m}} (1−m1)k=((1−m1)m)mk≈e−mk
插入 n n n 个元素某个位不置为 1 的概率:
( 1 − 1 m ) k n ≈ e − k n m \left(1 - \frac{1}{m}\right)^{kn} \approx e^{-\frac{kn}{m}} (1−m1)kn≈e−mkn
插入 n n n 个元素某个位被置为 1 的概率:
1 − ( 1 − 1 m ) k n ≈ 1 − e − k n m 1 - \left(1 - \frac{1}{m}\right)^{kn} \approx 1 - e^{-\frac{kn}{m}} 1−(1−m1)kn≈1−e−mkn
查询一个元素, k k k 次 hash 后误判的概率为都命中 1 的概率:
( 1 − ( 1 − 1 m ) k n ) k ≈ ( 1 − e − k n m ) k \left(1 - \left(1 - \frac{1}{m}\right)^{kn}\right)^k \approx \left(1 - e^{-\frac{kn}{m}}\right)^k (1−(1−m1)kn)k≈(1−e−mkn)k
结论:
布隆过滤器的误判率为:
f ( k ) = ( 1 − e − k n m ) k f(k) = \left(1 - e^{-\frac{kn}{m}}\right)^k f(k)=(1−e−mkn)k
由误判率公式可知:
- 在 k k k 一定的情况下,当 n n n 增加时,误判率增加
- 当 m m m 增加时,误判率减少
在 m m m 和 n n n 一定,在对误判率公式求导,使误判率尽可能小的情况下,可以得到最优 hash 函数个数:
k = m n ln 2 k = \frac{m}{n} \ln 2 k=nmln2
已知期望误判率 p p p 和插入数据个数 n n n,可以求 bit 长度:
m = − n ln p ( ln 2 ) 2 m = -\frac{n \ln p}{(\ln 2)^2} m=−(ln2)2nlnp
2.3 布隆过滤器的实现
cpp
#pragma once
#include <iostream>
#include <string>
#include <bitset>
#include <math.h>
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 = std::string,
class Hash1 = HashFuncBKDR,
class Hash2 = HashFuncAP,
class Hash3 = HashFuncDJB>
class BloomFilter
{
private:
public:
void Set(const K& key)
{
size_t hash1 = Hash1()(key) % M;
size_t hash2 = Hash2()(key) % M;
size_t hash3 = Hash3()(key) % M;
_bs.set(hash1);
_bs.set(hash2);
_bs.set(hash3);
}
bool Find(const K& key)
{
size_t hash1 = Hash1()(key) % M;
if (!_bs.test(hash1)) return false;
size_t hash2 = Hash2()(key) % M;
if (!_bs.test(hash2)) return false;
size_t hash3 = Hash3()(key) % M;
if (!_bs.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;
std::bitset<M> _bs;
};
布隆过滤器优缺点
- 优点:效率⾼,节省空间,相⽐位图,可以适⽤于各种类型的标记过滤
- 缺点:存在误判(在是不准确的,不在是准确的),不好⽀持删除
2.4 布隆过滤器应用
爬⾍系统中URL去重:
在爬⾍系统中,为了避免重复爬取相同的URL,可以使⽤布隆过滤器来进⾏URL去重。爬取到的URL可以通过布隆过滤器进⾏判断,已经存在的URL则可以直接忽略,避免重复的⽹络请求和数据处理。
垃圾邮件过滤:
在垃圾邮件过滤系统中,布隆过滤器可以⽤来判断邮件是否是垃圾邮件。系统可以将已知的垃圾邮件的特征信息存储在布隆过滤器中,当新的邮件到达时,可以通过布隆过滤器快速判断是否为垃圾邮件,从⽽提⾼过滤的效率。
预防缓存穿透:
在分布式缓存系统中,布隆过滤器可以⽤来解决缓存穿透的问题。缓存穿透是指恶意用户请求⼀个不存在的数据,导致请求直接访问数据库,造成数据库压⼒过⼤。布隆过滤器可以先判断请求的数据是否存在于布隆过滤器中,如果不存在,直接返回不存在,避免对数据库的⽆效查询。如下图示:

对数据库查询提效:
在数据库中,布隆过滤器可以⽤来加速查询操作。例如:⼀个app要快速判断⼀个电话号码是否注册过,可以使⽤布隆过滤器来判断⼀个⽤⼾电话号码是否存在于表中,如果不存在,可以直接返回不存在,避免对数据库进⾏⽆⽤的查询操作。如果在,再去数据库查询进⾏⼆次确认。
三、海量数据处理问题
3.1 如何在10亿个整数⾥⾯求最⼤的前100个?
这是一个经典topk问题,使用一个只包含100个元素的小根堆,依次遍历10亿个数据,当把小根堆打满后,再把每个遍历的元素与堆顶元素进行比较,因为堆顶元素是整个堆中最小的,所以当新元素大于堆顶元素时,堆顶元素出堆把新元素入堆。
3.2 给两个⽂件,分别有100亿个query,只有1G内存,如何找到两个⽂件交集?
分析:假设平均每个query字符串50byte,100亿个query就是5000亿byte,约等于500G(1G约等于10亿多Byte),哈希表/红⿊树等数据结构肯定是⽆能为⼒的。
解决⽅案1:
- 这个⾸先可以⽤布隆过滤器解决,⼀个⽂件中的query放进布隆过滤器,另⼀个⽂件依次查找,在的就是交集,问题就是找到交集不够准确,虽然交集⼀定被找到,但因为在的值会误判,可能存在一些非交集的元素。
解决⽅案2:
- 哈希切分,⾸先内存的访问速度远⼤于硬盘,⼤⽂件放到内存搞不定,那么我们可以考虑切分为⼩⽂件,再放进内存处理。
- 但是不要平均切分,因为平均切分以后,每个⼩⽂件都需要依次暴⼒处理,效率还是太低了。
- 可以利⽤哈希切分,依次读取⽂件中query, i = H a s h F u n c ( q u e r y ) % N i=HashFunc(query)\%N i=HashFunc(query)%N, N N N为准备切分多少分⼩⽂件, N N N取决于切成多少份,内存能放下,query放进第i号⼩⽂件,这样A和B中相同的query算出的hash值i是⼀样的,相同的query就进⼊的编号相同的⼩⽂件就可以编号相同的⽂件直接找交集,不⽤交叉找,效率就提升了。
- 本质是相同的query在哈希切分过程中,⼀定进⼊的同⼀个⼩⽂件 A i A_i Ai和 B i B_i Bi,不可能出现A中的的query进⼊ A i A_i Ai,但是B中的相同query进⼊了和 B j B_j Bj的情况,所以对 A i A_i Ai和 B i B_i Bi进⾏求交集即可,不需要 A i A_i Ai和 B j B_j Bj求交集。(本段表述中 i i i和 j j j是不同的整数)
哈希切分的问题就是每个⼩⽂件不是均匀切分的,可能会导致某个⼩⽂件很⼤内存放不下。我们细细分析⼀下某个⼩⽂件很⼤有两种情况:
- 1.这个⼩⽂件中⼤部分是同⼀个query。
- 2.这个⼩⽂件是有很多的不同query构成,本质是这些query冲突了。
针对情况1,其实放到内存的set中是可以放下的,因为set是去重的。
针对情况2,需要换个哈希函数继续⼆次哈希切分。所以本题我们遇到⼤于1G⼩⽂件,可以继续读到set中找交集,若set insert时抛出了异常(set插⼊数据抛异常只可能是申请内存失败了,不会有其他情况),那么就说明内存放不下是情况2,换个哈希函数进⾏⼆次哈希切分后再对应找交集。
注意: A i A_i Ai和 B i B_i Bi这些小文件都是放在磁盘里,等把它们拿到set里找交集时才是放在内存中。

3.3 给⼀个超过100G⼤⼩的log file,log中存着ip地址,设计算法找到出现次数最多的ip地址?如何查找出现次数前10的ip地址?
本题的思路跟上题类似,依次读取⽂件A中query, i = H a s h F u n c ( q u e r y ) % N i=HashFunc(query)\%N i=HashFunc(query)%N,query放进 A i A_i Ai号⼩⽂件,然后依次⽤map<string, int>对每个 A i A_i Ai⼩⽂件统计ip次数,同时求出现次数最多的ip或者topk ip。本质是相同的ip在哈希切分过程中,⼀定进⼊的同⼀个⼩⽂件A_i,不可能出现同⼀个ip进⼊ A i A_i Ai和 A j A_j Aj的情况,所以对 A i A_i Ai进⾏统计次数就是准确的ip次数

非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!🎉
四、测试源码
4.1 位图测试
cpp
#include <iostream>
#include "BitMap.hpp"
#include <ctime>
using namespace std;
// 测试函数
void TestBitMap() {
cout << "========== 位图测试开始 ==========" << endl;
// 测试1:基本功能测试
cout << "\n【测试1】基本功能测试" << endl;
BitMap bm(100); // 支持0-100范围内的数
// 测试set和find
bm.set(5);
bm.set(10);
bm.set(50);
bm.set(99);
cout << "设置数据:5, 10, 50, 99" << endl;
cout << "查找5:" << (bm.find(5) ? "存在" : "不存在") << endl;
cout << "查找10:" << (bm.find(10) ? "存在" : "不存在") << endl;
cout << "查找50:" << (bm.find(50) ? "存在" : "不存在") << endl;
cout << "查找99:" << (bm.find(99) ? "存在" : "不存在") << endl;
cout << "查找30:" << (bm.find(30) ? "存在" : "不存在") << endl;
// 测试erase
cout << "\n删除数据50" << endl;
bm.erase(50);
cout << "查找50:" << (bm.find(50) ? "存在" : "不存在") << endl;
// 测试2:边界测试
cout << "\n【测试2】边界测试" << endl;
BitMap bm2(32); // 刚好一个int
bm2.set(0);
bm2.set(31);
cout << "设置数据:0, 31" << endl;
cout << "查找0:" << (bm2.find(0) ? "存在" : "不存在") << endl;
cout << "查找31:" << (bm2.find(31) ? "存在" : "不存在") << endl;
// 测试3:重复设置测试
cout << "\n【测试3】重复设置测试" << endl;
BitMap bm3(64);
bm3.set(20);
cout << "第一次设置20,查找20:" << (bm3.find(20) ? "存在" : "不存在") << endl;
bm3.set(20); // 重复设置
cout << "第二次设置20,查找20:" << (bm3.find(20) ? "存在" : "不存在") << endl;
bm3.erase(20);
cout << "删除20后,查找20:" << (bm3.find(20) ? "存在" : "不存在") << endl;
bm3.erase(20); // 重复删除
cout << "再次删除20后,查找20:" << (bm3.find(20) ? "存在" : "不存在") << endl;
// 测试4:大量数据测试
cout << "\n【测试4】大量数据测试" << endl;
BitMap bm4(1000000); // 支持0-999999的数
const int TEST_COUNT = 10000;
int success_count = 0;
// 记录开始时间
clock_t start = clock();
// 设置测试数据
for (int i = 0; i < TEST_COUNT; i++) {
bm4.set(i * 100); // 设置0,100,200,...
}
// 验证数据
for (int i = 0; i < TEST_COUNT; i++) {
if (bm4.find(i * 100)) {
success_count++;
}
}
clock_t end = clock();
cout << "设置了" << TEST_COUNT << "个数据" << endl;
cout << "成功验证:" << success_count << "个" << endl;
cout << "耗时:" << (double)(end - start) * 1000 / CLOCKS_PER_SEC << "ms" << endl;
// 测试5:交替设置和删除测试
cout << "\n【测试5】交替设置和删除测试" << endl;
BitMap bm5(1000);
for (int i = 0; i < 10; i++) {
int num = i * 50;
bm5.set(num);
cout << "设置" << num << ",查找结果:" << bm5.find(num) << " ";
bm5.erase(num);
cout << "删除" << num << ",查找结果:" << bm5.find(num) << endl;
}
// 测试6:随机操作测试
cout << "\n【测试6】随机操作测试" << endl;
BitMap bm6(1000);
srand(time(nullptr));
for (int i = 0; i < 20; i++) {
int num = rand() % 1000;
int op = rand() % 3; // 0:set, 1:find, 2:erase
switch(op) {
case 0:
bm6.set(num);
cout << "设置 " << num << " -> 现在查找结果:" << bm6.find(num) << endl;
break;
case 1:
cout << "查找 " << num << " -> 结果:" << bm6.find(num) << endl;
break;
case 2:
bm6.erase(num);
cout << "删除 " << num << " -> 现在查找结果:" << bm6.find(num) << endl;
break;
}
}
cout << "\n========== 位图测试结束 ==========" << endl;
}
int main() {
TestBitMap();
return 0;
}
4.2 布隆过滤器测试
cpp
#include <iostream>
#include <string>
#include <vector>
#include <random>
#include <chrono>
#include <iomanip>
#include <bitset>
// 包含你的布隆过滤器头文件
#include "BloomFilter.hpp"
// 生成随机字符串
std::string randomString(int length) {
static const char charset[] =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_int_distribution<> dis(0, sizeof(charset) - 2);
std::string str;
for (int i = 0; i < length; ++i) {
str += charset[dis(gen)];
}
return str;
}
// 移到函数外部定义 - 标准版本
class BloomFilterWithCount {
private:
static const size_t M = 10000 * 5;
std::bitset<M> _bs;
public:
void Set(const std::string& key) {
size_t h1 = HashFuncBKDR()(key) % M;
size_t h2 = HashFuncAP()(key) % M;
size_t h3 = HashFuncDJB()(key) % M;
_bs.set(h1);
_bs.set(h2);
_bs.set(h3);
}
bool Find(const std::string& key) {
size_t h1 = HashFuncBKDR()(key) % M;
if (!_bs.test(h1)) return false;
size_t h2 = HashFuncAP()(key) % M;
if (!_bs.test(h2)) return false;
size_t h3 = HashFuncDJB()(key) % M;
if (!_bs.test(h3)) return false;
return true;
}
double getFillRatio() const {
return static_cast<double>(_bs.count()) / M;
}
};
// 移到函数外部定义 - 自定义版本
class BloomFilterCustom {
private:
static const size_t M = 10000 * 5;
std::bitset<M> _bs;
public:
void Set(const std::string& key) {
// 使用更多哈希函数
size_t h1 = HashFuncBKDR()(key) % M;
size_t h2 = HashFuncAP()(key) % M;
size_t h3 = HashFuncDJB()(key) % M;
size_t h4 = (h1 + h2) % M; // 组合哈希
size_t h5 = (h2 + h3) % M; // 组合哈希
_bs.set(h1);
_bs.set(h2);
_bs.set(h3);
_bs.set(h4);
_bs.set(h5);
}
bool Find(const std::string& key) {
size_t h1 = HashFuncBKDR()(key) % M;
if (!_bs.test(h1)) return false;
size_t h2 = HashFuncAP()(key) % M;
if (!_bs.test(h2)) return false;
size_t h3 = HashFuncDJB()(key) % M;
if (!_bs.test(h3)) return false;
if (!_bs.test((h1 + h2) % M)) return false;
if (!_bs.test((h2 + h3) % M)) return false;
return true;
}
double getFillRatio() const {
return static_cast<double>(_bs.count()) / M;
}
};
// 测试不同参数配置下的布隆过滤器
template<size_t N, size_t X>
void testBloomFilter(const std::vector<std::string>& insertData,
const std::vector<std::string>& testData) {
BloomFilter<N, X> bf;
// 计时开始
auto start = std::chrono::high_resolution_clock::now();
// 插入数据
for (const auto& key : insertData) {
bf.Set(key);
}
auto mid = std::chrono::high_resolution_clock::now();
// 查找数据并统计误判
int falsePositive = 0;
for (const auto& key : testData) {
if (bf.Find(key)) {
falsePositive++;
}
}
auto end = std::chrono::high_resolution_clock::now();
// 计算时间
auto insertTime = std::chrono::duration_cast<std::chrono::microseconds>(mid - start);
auto findTime = std::chrono::duration_cast<std::chrono::microseconds>(end - mid);
// 输出结果
std::cout << "参数配置: N=" << N << ", X=" << X << std::endl;
std::cout << " 理论误判率: " << std::fixed << std::setprecision(4)
<< bf.getFalseProbability() * 100 << "%" << std::endl;
std::cout << " 实际误判率: " << std::fixed << std::setprecision(4)
<< (double)falsePositive / testData.size() * 100 << "%"
<< " (" << falsePositive << "/" << testData.size() << ")" << std::endl;
std::cout << " 插入时间: " << insertTime.count() / 1000.0 << " ms" << std::endl;
std::cout << " 查找时间: " << findTime.count() / 1000.0 << " ms" << std::endl;
std::cout << std::string(50, '-') << std::endl;
}
int main() {
std::cout << "========== 布隆过滤器复杂测试 ==========" << std::endl;
// 测试参数
const int INSERT_COUNT = 10000; // 插入数据量
const int TEST_COUNT = 10000; // 测试数据量
const int STRING_LENGTH = 10; // 字符串长度
// 生成测试数据
std::cout << "生成测试数据..." << std::endl;
std::vector<std::string> insertData;
std::vector<std::string> testData;
std::random_device rd;
std::mt19937 gen(rd());
// 生成插入数据
for (int i = 0; i < INSERT_COUNT; ++i) {
insertData.push_back(randomString(STRING_LENGTH));
}
// 生成测试数据(一部分已存在,一部分不存在)
std::uniform_int_distribution<> dis(0, 1);
for (int i = 0; i < TEST_COUNT; ++i) {
if (i < TEST_COUNT / 2 && !insertData.empty()) { // 前50%用已存在的数据
std::uniform_int_distribution<> pick(0, INSERT_COUNT - 1);
testData.push_back(insertData[pick(gen)]);
} else { // 后50%生成随机字符串(应该不存在)
testData.push_back(randomString(STRING_LENGTH));
}
}
std::cout << "数据生成完成!" << std::endl;
std::cout << "插入数据量: " << INSERT_COUNT << std::endl;
std::cout << "测试数据量: " << TEST_COUNT << std::endl;
std::cout << std::string(50, '=') << std::endl;
// 测试1: 不同X值对误判率的影响
std::cout << "\n【测试1: 不同X值对误判率的影响】" << std::endl;
testBloomFilter<10000, 2>(insertData, testData); // 每个元素2位,空间小
testBloomFilter<10000, 5>(insertData, testData); // 每个元素5位,空间中等
testBloomFilter<10000, 10>(insertData, testData); // 每个元素10位,空间大
testBloomFilter<10000, 20>(insertData, testData); // 每个元素20位,空间很大
// 测试2: 不同数据量下的表现
std::cout << "\n【测试2: 不同数据量下的表现】" << std::endl;
// 重新生成不同规模的数据
std::vector<int> sizes = {100, 1000, 5000, 10000};
for (int size : sizes) {
std::vector<std::string> smallInsert;
for (int i = 0; i < size; ++i) {
smallInsert.push_back(randomString(STRING_LENGTH));
}
std::cout << "数据规模: " << size << std::endl;
BloomFilter<10000, 5> bf; // 固定空间大小
auto start = std::chrono::high_resolution_clock::now();
for (const auto& key : smallInsert) {
bf.Set(key);
}
auto end = std::chrono::high_resolution_clock::now();
auto time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << " 插入" << size << "个元素耗时: "
<< time.count() / 1000.0 << " ms" << std::endl;
}
// 测试3: 哈希函数组合的影响
std::cout << "\n【测试3: 不同哈希函数组合的影响】" << std::endl;
// 测试标准版本
std::cout << "标准版本(3个哈希函数):" << std::endl;
BloomFilterWithCount bfStandard;
auto start = std::chrono::high_resolution_clock::now();
for (const auto& key : insertData) {
bfStandard.Set(key);
}
auto mid = std::chrono::high_resolution_clock::now();
int falsePositiveStd = 0;
for (const auto& key : testData) {
if (bfStandard.Find(key)) {
falsePositiveStd++;
}
}
auto end = std::chrono::high_resolution_clock::now();
auto insertTimeStd = std::chrono::duration_cast<std::chrono::microseconds>(mid - start);
auto findTimeStd = std::chrono::duration_cast<std::chrono::microseconds>(end - mid);
std::cout << " 实际误判率: " << std::fixed << std::setprecision(4)
<< (double)falsePositiveStd / testData.size() * 100 << "%"
<< " (" << falsePositiveStd << "/" << testData.size() << ")" << std::endl;
std::cout << " 插入时间: " << insertTimeStd.count() / 1000.0 << " ms" << std::endl;
std::cout << " 查找时间: " << findTimeStd.count() / 1000.0 << " ms" << std::endl;
std::cout << " 填充率: " << bfStandard.getFillRatio() * 100 << "%" << std::endl;
// 测试增强版本
std::cout << "\n增强版本(5个哈希函数):" << std::endl;
BloomFilterCustom bfCustom;
start = std::chrono::high_resolution_clock::now();
for (const auto& key : insertData) {
bfCustom.Set(key);
}
mid = std::chrono::high_resolution_clock::now();
int falsePositiveCustom = 0;
for (const auto& key : testData) {
if (bfCustom.Find(key)) {
falsePositiveCustom++;
}
}
end = std::chrono::high_resolution_clock::now();
auto insertTimeCustom = std::chrono::duration_cast<std::chrono::microseconds>(mid - start);
auto findTimeCustom = std::chrono::duration_cast<std::chrono::microseconds>(end - mid);
std::cout << " 实际误判率: " << std::fixed << std::setprecision(4)
<< (double)falsePositiveCustom / testData.size() * 100 << "%"
<< " (" << falsePositiveCustom << "/" << testData.size() << ")" << std::endl;
std::cout << " 插入时间: " << insertTimeCustom.count() / 1000.0 << " ms" << std::endl;
std::cout << " 查找时间: " << findTimeCustom.count() / 1000.0 << " ms" << std::endl;
std::cout << " 填充率: " << bfCustom.getFillRatio() * 100 << "%" << std::endl;
// 测试4: 极端情况测试
std::cout << "\n【测试4: 极端情况测试】" << std::endl;
// 测试1: 空过滤器
BloomFilter<1000, 5> emptyBf;
std::cout << "空过滤器查找: "
<< (emptyBf.Find("anything") ? "存在" : "不存在") << std::endl;
// 测试2: 重复插入
BloomFilter<100, 5> dupBf;
dupBf.Set("same");
dupBf.Set("same"); // 重复插入
dupBf.Set("same");
std::cout << "重复插入后查找: "
<< (dupBf.Find("same") ? "存在" : "不存在") << std::endl;
// 测试3: 长字符串
std::string longStr(1000, 'a');
BloomFilter<100, 5> longBf;
longBf.Set(longStr);
std::cout << "长字符串查找: "
<< (longBf.Find(longStr) ? "存在" : "不存在") << std::endl;
// 测试4: 空字符串
BloomFilter<100, 5> emptyStrBf;
emptyStrBf.Set("");
std::cout << "空字符串查找: "
<< (emptyStrBf.Find("") ? "存在" : "不存在") << std::endl;
std::cout << "\n========== 测试完成 ==========" << std::endl;
return 0;
}
