位图与布隆过滤器:原理、实现与海量数据处理方案

文章目录

  • 一、位图
    • [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] 仅包含小写字母
  • 如果你不使用额外的数据结构,会很加分。
cpp 复制代码
    bool 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;
}
相关推荐
珠海西格2 小时前
4 月 1 日起执行分布式光伏监控新规,直接影响从业者与项目收益
大数据·运维·服务器·分布式·能源
宵时待雨2 小时前
C++笔记归纳13:map & set
开发语言·数据结构·c++·笔记·算法
1104.北光c°3 小时前
滑动窗口HotKey探测机制:让你的缓存TTL更智能
java·开发语言·笔记·程序人生·算法·滑动窗口·hotkey
仰泳的熊猫7 小时前
题目2570:蓝桥杯2020年第十一届省赛真题-成绩分析
数据结构·c++·算法·蓝桥杯
无极低码10 小时前
ecGlypher新手安装分步指南(标准化流程)
人工智能·算法·自然语言处理·大模型·rag
软件算法开发10 小时前
基于海象优化算法的LSTM网络模型(WOA-LSTM)的一维时间序列预测matlab仿真
算法·matlab·lstm·一维时间序列预测·woa-lstm·海象优化
罗超驿11 小时前
独立实现双向链表_LinkedList
java·数据结构·链表·linkedlist
superior tigre11 小时前
22 括号生成
算法·深度优先