哈希扩展 --- 布隆过滤器
什么是布隆过滤器
对于上一篇提到的位图,他是有一个比较严重的缺陷的,就是只适用于整型。哈希表/红黑树啥事都能搞,但是空间消耗很多,数据量很多的时候,红黑树就用不上了。所以如果是整型的话,就可以使用位图来解决,但是!不是位图呢???字符串?结构对象?这时候,位图就使不上劲了。
我们其实可以对位图进行变形,这就是布隆这个大佬提出来的这一种概念:
布隆过滤器(Bloom Filter)由 Burton Howard Bloom 于 1970 年提出,是一种紧凑且巧妙的概率型数据结构,专门用于在“海量非整型数据”中高效地判断“某元素一定不存在,或可能存在”。当数据量极大、元素本身不是整数、且内存无法容纳完整哈希表或红黑树时,传统位图无法直接使用,布隆过滤器便成为理想选择。
假设是字符串,就是通过某种哈希算法,将其转化成一个整型值,然后再通过这个整型值取映射到位图上!
但是则时候主要的问题就是哈希冲突,a和b字符串通过哈希算法出来的整型是不同的,但是来了一个c字符串,通过哈希算法转成的整型值是和a哈希算法后的整型值一致的!这就是哈希冲突!这时候也就有误判问题:就是通过位图知道a和b和c都在,但是只有a和b在,c不在的!所以我们判断一个 key 值在不在是不准确的,但是判断一个 key 值不在是准确的!
那布隆是怎么考量这个问题的呢?
它的核心思路分两步:
-
把任意类型的 key 通过 k 个独立哈希函数映射成 k 个整数;
-
将这 k 个整数对应到一张位图中的 k 个比特位并置 1。
插入时,重复上述两步即可完成;查询时,如果这 k 个比特位中有任意一位是 0,便可断言该 key 一定不存在;若全部为 1,则只能说 key 可能存在——存在误判概率(减少冲突,没有解决冲突)。由于布隆过滤器不保存原始值,只标记位,因此无法像哈希表那样解决冲突,而是通过增加哈希函数数量 k 来尽可能降低冲突率。这样,它在保证极高查询效率的同时,也节省了大量内存空间。
猪八戒是在的,但是孙悟空是不在的,但凡有一个位置不在,那么他就是不在!
关于哈希函数要有几个?位图开多大比较合适?
哈希函数肯定不是越多越好的,太多了的话,就会消耗过多的空间,而且大多是1的冲突的概率也会是直线的上升的。下面其实有一个推导:
布隆过滤器误判率推导
推导过程:
说明:这个比较复杂,涉及概率论、极限、对数运算、求导函数等知识.
假设:
-
:布隆过滤器的
长度。
-
:插入过滤器的元素个数。
-
:哈希函数的个数。
布隆过滤器哈希函数等条件下某个位设置为 1 的概率:
布隆过滤器哈希函数等条件下某个位设置不为 1 的概率:
在经过 次哈希后,某个位置依旧不为 1 的概率:
根据极限公式:
添加 n 个元素某个位置不置为 1 的概率:
添加 n 个元素某个位置置为 1 的概率:
查询一个元素,k 次 hash 后误判的概率为都命中 1 的概率:
结论:
布隆过滤器的误判率为:
由误判率公式可知,在 (哈希函数)一定的情况下,当
(插入数据) 增加时,误判率增加,
(布隆过滤器的长度) 增加时,误判率减少。
在 和
一定,在对误判率公式求导,误判率尽可能小的情况下,可以得到
数个数:
时误判率最低。
期望的误判率 和插入数据个数
确定的情况下,再把上面的公式带入误判率公式可以得到,通过期望的误判率和插入数据个数
得到
长度:
布隆过滤器代码实现
各种字符串Hash函数 - clq - 博客园https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html
下面代码是一个 C++ 实现的布隆过滤器(Bloom Filter)模板类,它使用了三种不同的哈希函数:BKDR、AP 和 DJB。这个布隆过滤器类允许您设置键(插入元素)并测试键(检查元素是否可能存在于集合中)。此外,还提供了一个计算误判概率的方法。
代码中定义了三个哈希函数结构体 HashFuncBKDR
、HashFuncAP
和 HashFuncDJB
,每个结构体都重载了 operator()
来实现特定的哈希算法。
布隆过滤器类 BloomFilter
模板参数包括:
-
N
:预计插入的数据个数。 -
X
:每个元素使用的哈希函数个数,默认为 5。 -
K
:键的类型,默认为std::string
。 -
Hash1
、Hash2
、Hash3
:三个哈希函数类型,默认分别为HashFuncBKDR
、HashFuncAP
和HashFuncDJB
。
类中的方法包括:
-
Set(const K& key)
:将键插入布隆过滤器。 -
Test(const K& key)
:测试键是否可能存在于布隆过滤器中。 -
getFalseProbability()
:计算并返回理论误判率。
在 TestBloomFilter1
函数中,创建了一个布隆过滤器实例并插入了一些字符串,然后测试了这些字符串以及一些不在集合中的字符串。
在 TestBloomFilter2
函数中,进行了更大规模的测试,包括相似字符串和不相似字符串的误判率测试,并与理论误判率进行了比较。
#pragma once
#include<string>
#include"BitSet.h"// BKDR Hash Function 结构体定义
struct HashFuncBKDR
{// BKDR 哈希算法,由 Brian Kernighan 和 Dennis Ritchie 提出// 该算法在《The C Programming Language》一书中展示,Java 字符串哈希也采用此算法size_t operator()(const std::string& s){size_t hash = 0;for (auto ch : s){hash *= 31;hash += ch;}return hash;}
};// AP Hash Function 结构体定义
struct HashFuncAP
{// AP 哈希算法,由 Arash Partow 发明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;}
};// DJB Hash Function 结构体定义
struct HashFuncDJB
{// DJB 哈希算法,由 Daniel J. Bernstein 教授发明size_t operator()(const std::string& s){size_t hash = 5381;for (auto ch : s){hash = hash * 33 ^ ch;}return hash;}
};// BloomFilter 类模板定义
template<size_t N,size_t X = 5,class K = std::string,class Hash1 = HashFuncBKDR,class Hash2 = HashFuncAP,class Hash3 = HashFuncDJB>
class BloomFilter
{
public:// 将 key 插入布隆过滤器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);}// 测试 key 是否可能存在于布隆过滤器中bool Test(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:// 位图大小,根据预计插入的数据个数 N 和每个元素使用的哈希函数个数 X 计算得出static const size_t M = N * X;// 使用 BitSet 库来实现位图bit::bitset<M> _bs;
};// 测试 BloomFilter 类的函数
void TestBloomFilter1()
{BloomFilter<10> bf;bf.Set("猪八戒");bf.Set("孙悟空");bf.Set("唐僧");std::cout << bf.Test("猪八戒") << std::endl;std::cout << bf.Test("孙悟空") << std::endl;std::cout << bf.Test("唐僧") << std::endl;std::cout << bf.Test("沙僧") << std::endl;std::cout << bf.Test("猪八戒1") << std::endl;std::cout << bf.Test("猪戒八") << std::endl;
}// 另一个测试 BloomFilter 类的函数
void TestBloomFilter2()
{srand(time(0));const size_t N = 1000000;BloomFilter<N> bf;std::vector<std::string> v1;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;}}std::cout << "相似字符串误判率:" << (double)n2 / (double)N << std::endl;// 不相似字符串集 前缀后缀都不一样v1.clear();for (size_t i = 0; i < N; ++i){std::string urlstr = "孙悟空";urlstr += std::to_string(i + rand());v1.push_back(urlstr);}size_t n3 = 0;for (auto& str : v1){if (bf.Test(str)){++n3;}}std::cout << "不相似字符串误判率:" << (double)n3 / (double)N << std::endl;std::cout << "公式计算出的误判率:" << bf.getFalseProbability() << std::endl;
}
PS D:\2024C语言\C++加餐\c-additional-meal\哈希扩展学习加餐> ./test
相似字符串误判率:0.127968
不相似字符串误判率:0.104435
公式计算出的误判率:0.091236
当然了,我们这里没有动态的去映射,后续我们可以更好的完善这个布隆过滤器!
布隆过滤器删除问题
布隆过滤器默认是不支持删除的,因为例如“猪八戒”和“孙悟空”都映射在布隆过滤器中,他们映射的位有一个位是共同映射的(冲突的),如果我们把孙悟空删掉,那么再去查找“猪八戒”会查找不到,因为那么“猪八戒”间接被删掉了。
解决方案:可以考虑引用计数/计数标记的方式,一个位置用多个位标记,记录映射这个位的计数值,删除时,仅仅减减计数,那么就可以某种程度支持删除。但是这个方案也有缺陷,如果一个值不在布隆过滤器中,我们去删除,减减了映射位的计数,那么会影响已存在的值,也就是说,一个确定存在的值,可能会变成不存在,这里就很坑,因为在极端情况下,我们是使用了Test去判断在不在,但是可能会误判啊!!!当然也有人提出,我们可以考虑计数方式支持删除,但是定期重建一下布隆过滤器,这样也是一种思路。
布隆过滤器的应用
首先我们分析一下布隆过滤器的优缺点:
优点:
-
效率高,节省空间,相比位图,可以适用于各种类型的标记过滤。
缺点:
-
存在误判(在是不准确的,不在是准确的)。
-
不好支持删除。
布隆过滤器在实际中的一些应用:
-
爬虫系统中URL去重: 在爬虫系统中,为了避免重复爬取相同的URL,可以使用布隆过滤器来进行URL去重。爬取到的URL可以通过布隆过滤器进行判断,已经存在的URL则可以直接忽略,避免重复的网络请求和数据处理。(因为有时候会有环形网站的存在,可能存在死循环)
-
垃圾邮件过滤: 在垃圾邮件过滤系统中,布隆过滤器可以用来判断邮件是否是垃圾邮件。系统可以将已知的垃圾邮件的特征信息存储在布隆过滤器中,当新的邮件到达时,可以通过布隆过滤器快速判断是否为垃圾邮件,从而提高过滤的效率。
-
预防缓存穿透: 在分布式缓存系统中,布隆过滤器可以用来解决缓存穿透的问题。缓存穿透是指恶意用户请求一个不存在的数据,导致请求直接访问数据库,造成数据库压力过大。布隆过滤器可以先判断请求的数据是否存在于布隆过滤器中,如果不存在,直接返回不存在,避免对数据库的无效查询。类似redis就是缓存!对热数据的中间层的缓存!
-
对数据库查询提效: 在数据库中,布隆过滤器可以用来加速查询操作。例如:一个app要快速判断一个电话号码是否注册过,可以使用布隆过滤器来判断一个用户电话号码是否存在于表中,如果不存在,可以直接返回不存在,避免对数据库进行无用的查询操作。如果在,再去数据库查询进行二次确认。
布隆过滤器其实应用听广泛的!!!