> 文档中心 > 【C++进阶】第二十四篇——布隆过滤器(概念+实现+海量数据处理问题)

【C++进阶】第二十四篇——布隆过滤器(概念+实现+海量数据处理问题)


⭐️这篇博客是对上一篇博客中进行扩展,我们应该知道,位图的效率确实很高,但是有一个致命的缺点——只能对整形数据进行处理。今天要介绍的布隆过滤器就是来解决这个缺点的,看完下面的内容,相信你会了解这个数据结构。
⭐️博客代码已上传至gitee:https://gitee.com/byte-binxin/cpp-class-code

目录

  • 🌏引入
  • 🌏概念
  • 🌏实现
    • 🌲整体框架
    • 🌲插入
    • 🌲查找
    • 🌲删除
    • 🌲测试代码
  • 🌏布隆过滤器的优缺点
  • 🌏海量数据处理问题
  • 🌐总结

🌏引入

我们在使刷抖音时,它会给我们不停地推荐新的内容,而且推荐的内容基本上不会出现我们看过的内容。那这个效果是如何实现去重的呢?用户服务器会把用户看过的内容进行标记,也就是保存在历史记录中,当每次要推荐内容时,服务器都要先去用户的历史记录中查找当前内容是否在历史记录中,在就过滤掉,不在就将该内容推送给用户。

如何快速查找?

  1. 哈希表: 用哈希表存储用户的历史记录。缺点: 空间消耗比较大
  2. 位图: 用无图存储用户的历史记录。 缺点: 不能处理哈希冲突问题
  3. 布隆过滤器: 将哈希表和位图结合使用。

🌏概念

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。它通过多个哈希函数将一个数据映射到位图的结构中(也就是一个数据映射位图的多个位置,这样就可以减少冲突的概率)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难

在这里插入图片描述

🌏实现

🌲整体框架

这里用到了上一篇博客中的位图实现,其中这里放置了3个哈希函数,用了映射不同的概率。

哈希函数个数和布隆过滤器长度的关系:

m = -n*ln(p) / (ln(2)^2) k = m/n * ln(2)# k 为哈希函数个数# m 为布隆过滤器长度# n 为插入的元素个数# p 为可接受该容器的误报率(0-1)

其中k=3,整体算下来,m≈4.34n,所以这里我们选择m为5。

template<class T = string, class Hash1 = BKDRHash, class Hash2 = SDBHash, class Hash3 = RSHash>class BloomFilter{public:// 布隆过滤器的长度 近似等于4.3~5倍插入元素的个数// 这里取 5BloomFilter(size_t size):_bs(5*size), _N(5 * size){}private:bitset _bs;size_t _N;// 能够映射元素个数 };

三个字符串哈希函数如下: 用他们作为缺省参数,默认处理字符串类型

// BKDRHashstruct BKDRHash{size_t operator()(const string& str){register size_t hash = 0;for (size_t i = 0; i < str.length(); ++i){hash = hash * 131 + str[i];}return hash;}};// SDBHashstruct SDBHash{size_t operator()(const string str){register size_t hash = 0;for (size_t i = 0; i < str.length(); ++i){hash = 65599 * hash + str[i];//hash = (size_t)ch + (hash << 6) + (hash << 16) - hash;  }return hash;}};// RSHashstruct RSHash{size_t operator()(const string str){register size_t hash = 0;size_t magic = 63689;for (size_t i = 0; i < str.length(); ++i){hash = hash * magic + str[i];magic *= 378551;}return hash;}};

🌲插入

步骤

  1. 先用不同的哈希函数计算出该数据分别要映射到位图的哪几个位置
  2. 然后把位图中的这几个位置设置为1

代码实现:

void set(const T& x){size_t index1 = Hash1()(x) % _N;size_t index2 = Hash2()(x) % _N;size_t index3 = Hash3()(x) % _N;_bs.set(index1);_bs.set(index2);_bs.set(index3);}

🌲查找

布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1。

步骤:

  1. 先用不同的哈希函数计算出该数据分别要映射到位图的哪几个位置
  2. 然后判断位图中的这几个位置是否都为1,如果有一个不为1,说明该元素一定不在容器中,否则表示在容器中

注意: 可能会误报,判断在是不准确的,判断不在是准确的。(因为一个数据判断出它是在的,可能是它映射的几个数据可能是其它数据映射导致这几个位置为1的,所以判断结果为在,该结果有误判;而判断一个数据为不在时,那这个数据是一定不在的,因为它映射的几个位置不全为1)

代码实现如下:

bool IsInBloomFilter(const T& x){size_t index1 = Hash1()(x) % _N;size_t index2 = Hash2()(x) % _N;size_t index3 = Hash3()(x) % _N;return _bs.test(index1)&& _bs.test(index2)&& _bs.test(index3);// 可能会误报,判断在是不准确的,判断不在是准确的}

🌲删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。也就是说,要删除的元素映射的位置可能会是其它元素映射的位置,所以直接删除元素会给后期查找某个元素带来极大的误判。

解决: 可以考虑给把比特位(根据需求选择用多少个比特位)扩展成为一个小的计数器,插入元素时,给计数器加1,删除元素时,给计数器减1。当然这个肯定会带来更大的空间开销,牺牲了布隆过滤器的优势。且还可能会存在计数回绕的问题,如果次数超过计数器的范围,就会溢出。

🌲测试代码

代码如下:

void TestBloomFilter(){BloomFilter<string> bf(100);bf.set("douyin");bf.set("kuaishou");bf.set("pass cet6");bf.set("aabb");cout << bf.IsInBloomFilter("pass cet6") << endl;cout << bf.IsInBloomFilter("kuaishou") << endl;cout << bf.IsInBloomFilter("douyin") << endl;cout << bf.IsInBloomFilter("abab") << endl;}

代码运行结果如下:
在这里插入图片描述

🌏布隆过滤器的优缺点

优点:

  1. 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
  2. 哈希函数相互之间没有关系,方便硬件并行运算
  3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
  4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
  5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
  6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算

缺点:

  1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
  2. 不能获取元素本身
  3. 一般情况下不能从布隆过滤器中删除元素
  4. 如果采用计数方式删除,可能会存在计数回绕问题

🌏海量数据处理问题

哈希切割

  1. 给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 如何找到top K的IP?如何直接用Linux系统命令实现?

a. 先创建1000个小文件A0-A999,然后计算i = hash(IP)%1000,i是多少,IP就进入编号为i的文件中,也就是Ai文件中,这样相同的IP就都进入了同一个文件中。先将一个小文件加载到内存中,依次读取放入unordered_map 中,同时用一个pair max记录当前出现次数最多的IP,然后不断更新记录当前的max,这样就得到出现次数最多的IP地址
b. 和上面的方法一样,这里用一个大小为K的堆来存储topK的IP

位图应用

  1. 给定100亿个整数,设计算法找到只出现一次的整数?

100亿个整数占用40G的空间,如果直接加载到内存中,空间肯定是不够的,所以我们这里可以考虑用位图来处理。
方案一: 改进位图,用两个比特位表示整数,用其中的三种状态00(没出现)、01(出现一次)和10(出现两次及以上)。消耗内存为:24(2^32-1)/32byte≈1G
方案二: 用两个位图同时操作,对应比特位无数据时,两个位图此处设为0,有一个数据时,将两个位图中一个设为0,一个设为1,有两个及以上数据时,两个位图中对于比特位都设置为1。消耗空间和上面那种方法一样,也是1G

  1. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?

方案一: 将文件1的整数映射到一个位图中,然后读取文件2中的数据,判断是否在位图中,在就是交集,消耗内存500M
方案二: 将文件1的整数映射到一个位图中, 将文件2的整数映射到另一个位图中,然后将两个位图进行按位与,与之后位图中为1的位就是两个文件的交集

  1. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数

方案: 和第一题思路一致,找出状态为00、01和10的即可,其中状态为11代表的是出现3次及以上。消耗内存为1G

布隆过滤器

  1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法

query是一个查询语句的意思,一个query语句的大小平均约为30-60字节100亿个query大约占用300-600G。
方案一(近似算法): 将文件1的query映射到布隆过滤器中,读取文件2中的query,判断是否在布隆过滤器中,在就是交集,是交集的一定在里面。(缺陷:交集中的数会不准确,因为有些数会误判,混到交集里面,判断在是不准确的,判断不在是准确的
方案二(精确算法):
1.把文件1和文件2分别切分成A0、A1…A999和B0、B1…B999,两个文件分别切分成1000个小文件,然后将A0加载到内存放到unordered_set中,再依次和B0、B1…B999这10001个文件进行比较,然后加载A1到内存中,以此类推。用unordered_set的效率还是很高的,基本上是常数次。
在这里插入图片描述
2. 优化:哈希切分
不再使用平均切分,使用哈希切分,用公式:i = Hash(query)%1000。i是多少就进入编号为i的文件中。不同的query可能会进入编号不同的文件中,但相同的query一定会进入编号相同的文件中。
先将文件Ai加载到内存中,放到unordered_set中,然后读取Bi中query,判断是否在,在就是交集。这里只需要比较1000次,比上面那种方式比较次数少很多。

  1. 如何扩展BloomFilter使得它支持删除元素的操作

方案: 用几个比特位来表示计数器。缺陷:给的为如果少了,容易导致计数溢出,如果位数多了,那么空间开销就会很多,是以牺牲布隆过滤器的优势为代价的。

🌐总结

今天的内容就介绍到这里了,欢迎点赞、支持和关注~
在这里插入图片描述