✨✨ 欢迎大家来到贝蒂大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:C++学习
贝蒂的主页:Betty’s blog
1. 布隆过滤器的引入
在我们注册游戏或者社交账号时,我们可以自己设置昵称,但为了保证每个用户昵称的唯一性,我们必须检测输入的昵称是否被使用过,这本质其实就是一个key
的模型。一般而言,我们有两种解决方案:
方案一:
用红黑树或者哈希表存储相关数据,当判断一个数据是否存在时,可以极快的效率在红黑树或哈希表中查找。
方案二:
用位图将存储相关书籍,虽然位图只能存储整型数据,但我们可以通过一些哈希算法将字符串转换成整型,比如
BKDR
哈希算法。这种方法同样也能以极快的效率查找数据。
但是这两种方案其实都有一些缺点,当数量太大时因为红黑树与哈希表要存储相关信息,内存会不足,而如果用位图存储,虽然节约了大量空间,但是一个无符号整数最大值为4294967295
,而字符串的种类却是无限的,以无限对有限,无论哪种哈希算法都必然会导致哈希冲突。
所以为了解决这个问题,就有人提出一种结构——布隆过滤器。
2. 布隆过滤器的概念
布隆过滤器是由**布隆(Burton Howard Bloom)**在1970
年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。其具有以下几个特点:
- 布隆过滤器是位图的变形与延伸,虽无法避免哈希冲突,但可降低误判概率。
- 在布隆过滤器中,当一个数据映射到位图时,会使用多个哈希函数将其映射到多个比特位。判断数据是否在位图中,需依据这些哈希函数计算对应的比特位,若这些比特位全为 1,则判定数据存在,否则判定数据不存在。
- 布隆过滤器采用多个哈希函数进行映射,目的是降低哈希冲突概率。单个哈希函数产生冲突的概率可能较大,而多个哈希函数同时产生冲突的概率则较小。
- 布隆过滤器在判断一个数据是否存在时如果存在可能出现误判,但是如果不存在那一定没有误判。
比如说我们分别将Betty1
,Betty2
,Betty3
利用三个哈希函数映射进位图中,其分布可能为:
其中Betty1
,Betty2
,Betty3
这三个字符串都没有发生冲突。而如果三个哈希函数的计算结果都相同的话,那就可能造成哈希冲突,比如接下来的Betty1
与Betty2
。
其中如果某个位置是0的话,那该数据一定不存在因为没有任何数据指向这个位置。
为了降低布隆过滤器的误判率,有人就对布隆过滤器的长度与哈希函数的个数做了研究。得到一个公式
m
=
−
n
ln
p
/
(
l
n
2
)
2
,
k
=
m
l
n
2
/
n
m=-n \ln p/(ln 2)^2 , k = mln 2/n
m=−nlnp/(ln2)2,k=mln2/n。其中k
为哈希函数个数,m
为布隆过滤器长度,n
为插入的元素个数,p
为误判率。
如果使用的哈希函数为3个,那么根据公式
m
=
n
k
/
ln
2
≈
4
n
m=nk/\ln 2≈4n
m=nk/ln2≈4n,也就是说当布隆过滤器的长度是插入元素个数的4倍时误差最小。
3. 布隆过滤器的实现
3.1 布隆过滤器的结构
首先布隆过滤器肯定是一个模版类,有一个非类型模版参数控制长度,默认处理的对象为string
。默认也提供三个哈希函数,其成员变量也是一个位图。
template<size_t N, class K=string,class Hash1=BKDRHash,
class Hash2 = APHash, class Hash3 = DJBHash>
class BloomFilter
{
public:
//成员函数
private:
bitset<N> _bit;
};
3.2 布隆过滤器的插入
布隆过滤器的插入即通过不同的哈希函数计算对应的下标,然后进行相应的映射关系。
//成员函数
void set(const K& key)
{
//计算机对应的下标
size_t hashi1 = Hash1()(key)% N;
size_t hashi2 = Hash2()(key)% N;
size_t hashi3 = Hash3()(key)% N;
_bit.set(hashi1);
_bit.set(hashi2);
_bit.set(hashi3);
}
3.3 布隆过滤器的测试
布隆过滤器的删除即通过不同的哈希函数计算对应的下标,然后检测相应下标的状态。如果有一个下标不存在,那么这个数据肯定不存在,但是如果都存在也有可能有误差。
bool test(const K& key)
{
size_t hashi1 = Hash1()(key) % N;
size_t hashi2 = Hash2()(key) % N;
size_t hashi3 = Hash3()(key) % N;
if (!_bit.test(hashi1)||!_bit.test(hashi2)||!_bit.test(hashi3))
{
//一定不存在
return false;
}
return true;//可能存在误判
}
3.4 布隆过滤器的删除
布隆过滤器一般不能直接支持删除工作,原因是删除一个元素可能影响其他元素,如删除Betty1
可能导致Betty2
也被误删,因为二者可能在多个哈希函数计算出的比特位上有重叠。
如果一定要支持删除操作的话,一种支持删除的方法是将布隆过滤器的每个比特位扩展成小计数器(一个下标对应多个比特位),插入元素时给相应位置计数器加一,删除时减一,以多占用几倍存储空间为代价实现删除操作。但是仍然会存在几个问题:
- 如果你的位数给的不合适,可能某一次次数更新之后就会溢出,造成计数回绕(计数器值增加到达其最大范围后,再次增加会导致计数器值重新回到初始状态)。
- 并且查找一个元素的时候无法确认该元素是否真的存于布隆过滤器中。因为我们删除一个元素的时候一定要确保它是存在的,再去删除(减去对应位置的次数),不存在是不能删除的,但是判断一个元素是否在布隆过滤器中是可能误判的。所以我们在删除一个元素的时候无法确认它是否存在
所以一般而言布隆过滤器的删除操作是不可行的。
4. 布隆过滤器的优缺点
一、布隆过滤器的优点
- 时间复杂度低:增加和查询元素的时间复杂度为 O(K)(K 为哈希函数个数且一般较小),与数据量大小无关。
- 便于硬件并行运算:哈希函数相互之间没有关系。
- 保密优势:不需要存储元素本身,在保密要求严格的场合有很大优势。
- 空间优势:在能承受一定误判时,比其他数据结构有很大的空间优势。
- 可表示全集:数据量很大时可以表示全集,其他数据结构不能。
- 可进行运算:使用同一组散列函数的布隆过滤器可以进行交、并、差运算。
二、布隆过滤器的缺点
- 存在误判率:有假阳性,不能准确判断元素是否在集合中,可通过建立白名单补救。
- 不能获取元素本身。
- 一般情况下不能删除元素,采用计数方式删除可能会存在计数回绕问题。
5. 布隆过滤器的应用场景
一般而言使用布隆过滤器的前提是,布隆过滤器的误判不会对业务逻辑造成影响。以下是一个布隆过滤器的具体使用场景:
在电商平台的商品推荐系统中,当用户浏览商品时,系统会根据用户的历史浏览记录和购买行为进行个性化推荐。 假设用户的历史浏览记录和购买行为数据存储在数据库中,直接遍历数据库进行推荐计算会非常耗时,影响用户体验。这时可以使用布隆过滤器,将用户已经浏览过或购买过的商品 ID 全部添加到布隆过滤器当中。 当用户打开某个商品页面时,系统首先在布隆过滤器中查找该商品 ID。如果在布隆过滤器中查找后发现该商品 ID 不存在,说明用户没有浏览过或购买过这个商品,可以将其作为潜在的推荐商品进行初步推荐,避免了磁盘 IO。如果在布隆过滤器中查找后发现该商品 ID 存在,此时还需要进一步访问磁盘,复核用户是否真的浏览过或购买过该商品,因为布隆过滤器可能会有误判。 由于大部分情况下,系统推荐给用户的商品都是用户没有接触过的,所以在布隆过滤器中查找后通常都是找不到的,此时就避免了进行磁盘 IO。而只有在布隆过滤器误判或用户忘记自己浏览过或购买过某个商品的情况下,才需要访问磁盘进行复核。
6. 源码
#pragma once
#include<bitset>
struct BKDRHash
{
size_t operator()(const string& s)
{
size_t value = 0;
for (auto ch : s)
{
value = value * 131 + ch;
}
return value;
}
};
struct APHash
{
size_t operator()(const string& s)
{
size_t value = 0;
for (size_t i = 0; i < s.size(); i++)
{
if ((i & 1) == 0)
{
value ^= ((value << 7) ^ s[i] ^ (value >> 3));
}
else
{
value ^= (~((value << 11) ^ s[i] ^ (value >> 5)));
}
}
return value;
}
};
struct DJBHash
{
size_t operator()(const string& s)
{
if (s.empty())
return 0;
size_t value = 5381;
for (auto ch : s)
{
value += (value << 5) + ch;
}
return value;
}
};
template<size_t N, class K=string,class Hash1=BKDRHash,
class Hash2 = APHash, class Hash3 = DJBHash>
class BloomFilter
{
public:
//成员函数
void set(const K& key)
{
//计算机对应的下标
size_t hashi1 = Hash1()(key)% N;
size_t hashi2 = Hash2()(key)% N;
size_t hashi3 = Hash3()(key)% N;
_bit.set(hashi1);
_bit.set(hashi2);
_bit.set(hashi3);
}
bool test(const K& key)
{
size_t hashi1 = Hash1()(key) % N;
size_t hashi2 = Hash2()(key) % N;
size_t hashi3 = Hash3()(key) % N;
if (!_bit.test(hashi1)||!_bit.test(hashi2)||!_bit.test(hashi3))
{
//一定不存在
return false;
}
return true;//可能存在误判
}
private:
bitset<N> _bit;
};