布隆过滤器
1.布隆过滤器
1.1什么是布隆过滤器
- 引言
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记
录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
- 用哈希表存储用户记录,缺点:浪费空间
- 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。
- 将哈希与位图结合,即布隆过滤器
- 布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结
构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函
数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
1.2布隆过滤器的核心思想是什么
- 其核心思想是使用多个独立的哈希函数和一个位数组来表示集合中的元素。当要插入一个元素时,通过将元素经过多个哈希函数计算得到多个哈希值,并将对应位数组中的对应位置设置为1。当要查询一个元素是否存在时,同样通过多个哈希函数计算得到多个哈希值,并检查对应位数组中的对应位置是否都为1,若有任何一个位置为0,则可以确定元素一定不在集合中;若所有位置都为1,则可能存在误判,即元素可能在集合中。
1.3布隆过滤器优缺点是什么
1.3.1优点:
- 空间效率高:布隆过滤器使用一个位数组和多个哈希函数来表示集合,不需要存储实际元素的信息,因此占用的空间较小。
- 查询速度快:查询一个元素只需进行多次位数组的访问和比较,时间复杂度很低,不受集合大小的影响。
- 支持高并发:布隆过滤器的查询操作是无锁的,并且可以并行处理多个查询请求。
- 简单高效:实现起来相对简单,只需设计好哈希函数和位数组即可。
1.3.2缺点:
- 误判率存在:布隆过滤器可能会误判一个元素属于集合,但实际上不属于,这是因为多个元素可能映射到相同的位数组位置。误判率取决于位数组的大小和哈希函数的个数。
- 不支持删除操作:布隆过滤器中的元素无法被删除,因为删除一个元素会影响其他元素的判断结果。如果需要删除元素,需要使用其他数据结构来支持。
- 需要调整参数:为了降低误判率,布隆过滤器的位数组大小和哈希函数的个数需要根据预期元素数量和误判率进行合理调整,选择不当可能会导致误判率过高或空间占用过大。
- 无法提供元素的具体信息:布隆过滤器只能告诉我们元素可能存在于集合中,但无法提供元素的具体信息,如果需要获取元素的详细信息,还需要通过其他方式进行查询。
需要根据具体的应用场景和需求来评估布隆过滤器的适用性,并在设计和使用过程中注意参数的选择和误判率的控制。
1.3.3补充
-
布隆过滤器误判
布隆过滤器是无法杜绝误判的情况的,因为其核心实现的原因,举例说明:如我要在过滤器存一些相识数据 例如“addddddc”和“addddddddx”这两个数,这两个数通过一个HashFuc(哈希函数)计算后得到的结果都是5,那么只将其中一个数据“addddddc”加载进内存中,在内存中5这个位置就被标记为1(代表“addddddc”这个数存在),那么在查询“addddddddx”是否存在时,也是通过相同的一个HashFuc反向去查找,得到位置5的标记为1(存在),而实际上“addddddddx”这个数是不存在的,这就造成了布隆过滤器的误判,** 在实际中是无法杜绝的这个误判,类似于哈希冲突,但是我们可以通过一些手段来减少误判 **,下面布隆过滤器的性能会进行介绍。 -
不支持删除操作
其实布隆过滤器不是不支持删除操作,而是它的删除操作达不到我们的预期。下面进行举例说明:
设计一个计数型布隆过滤器(Counting Bloom Filter):计数型布隆过滤器在位数组的每个位置不仅存储一个二进制位,而是存储一个计数器,用于记录元素的出现次数。当需要删除一个元素时,可以将对应位置的计数器减少1。当计数器减为0时,可以将该位置置为0,表示该元素已被删除。然而,使用计数型布隆过滤器会增加存储空间和查询时间,因为需要存储更多的计数器信息和进行计数器的更新操作。
在上面这张图中,可以看到,经过相关的hash函数计算后" longly"和 "wofl"这两个单词在过滤器的内存都指向4这个位置,那么就同时把4这个地方标记成1 该位置的conut为2,那么我们如果进行删除操作的话,如把"longly"删除,删除之后把位置4的count–就表示删除,那么再进行查找操作时,这里的标记依旧是1,表示存在,所以达不到预期效果。
1.4影响布隆过滤器性能的因素有什么
1.4.1影响因素
-
哈希函数的数量和质量:布隆过滤器依赖哈希函数将元素映射到位数组中的位置。哈希函数的数量越多,可以提高布隆过滤器的准确性和降低误判率。同时,哈希函数的质量也很重要,要尽量保证哈希函数产生均匀分布的哈希值,减少冲突。
-
布隆过滤器的大小:布隆过滤器的大小(位数组的长度)直接影响其容量和误判率。较小的布隆过滤器可能会导致较高的误判率,而较大的布隆过滤器会增加存储开销。
-
插入操作的负载因子:布隆过滤器的负载因子是指实际插入元素数量与位数组长度之比。较高的负载因子可能会导致冲突增加,从而影响准确性和误判率。
-
误判率的容忍度:布隆过滤器是一种概率性数据结构,其存在一定的误判率。根据具体需求,可以根据容忍的误判率设定合适的参数,如哈希函数数量和布隆过滤器大小。
-
删除操作的处理:布隆过滤器本身不支持删除操作,而删除操作可能会引入额外的存储和查询开销。使用辅助数据结构或其他技术进行删除操作可能会影响性能。
1.4.2改进措施
- 使用多个不同的HashFuc来减少误判
我们知道,数据在过滤器中的映射关系是通过哈希函数来计算相关的位置的,跟哈希表解决冲突的方法相识,我们可以增加不同的哈希函数来映射,这就可以大大减少误判。相关哈希函数的性能介绍:链接: 常用哈希函数以及性能介绍,可以根据实际应该来选择一个或多个合适的哈希函数来构造映射关系。 - 怎么确定哈希函数的个数
1.5布隆过滤器的使用场景
-
网页缓存:在网页缓存中,布隆过滤器可以用来判断某个 URL 是否已经被缓存,避免重复的网络请求。
-
邮件服务器:在邮件服务器中,布隆过滤器可以用来过滤垃圾邮件。已知的垃圾邮件发送者的邮箱地址可以被添加到布隆过滤器中,来快速判断某个邮件发送者是否为垃圾邮件发送者。
-
URL 重复判定:在网页爬取系统中,布隆过滤器可以用来判断某个 URL 是否已经被爬取过,避免重复爬取相同的网页。
-
黑名单过滤:布隆过滤器可以用于快速判断某个数据是否在黑名单中,例如 IP 地址、手机号码等。
-
缓存穿透处理:在缓存系统中,布隆过滤器可以用来判断某个请求的数据是否存在于缓存中,从而避免缓存穿透问题。
-
大数据分析:在大数据分析中,布隆过滤器可以用来过滤掉已知的无关数据,以便更快地处理有意义的数据。
2.代码实现
2.1插入算法
下面代码是自己手撕的 ,测试可以使用第三方库的 (包括 Boost 库中的 boost::bloom_filter 和 Google 的 C++ Bloom Filters 等)
#pragma once
#include<string>
#include<bitset>
#include<iostream>
namespace GXPYY
{
//设置相关的哈希函数
//哈希函数1
struct BKDRHash
{
size_t operator()(const std::string& s)
{
// BKDR
size_t value = 0;
for (auto ch : s)
{
value *= 31;
value += ch;
}
return value;
}
};
//哈希函数2
struct APHash
{
size_t operator()(const std::string& s)
{
size_t hash = 0;
for (long 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;
}
};
//哈希函数3
struct DJBHash
{
size_t operator()(const std::string& s)
{
size_t hash = 5381;
for (auto ch : s)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
//哈希函数4
struct JSHash
{
size_t operator()(const std::string& s)
{
size_t hash = 1315423911;
for (auto ch : s)
{
hash ^= ((hash << 5) + ch + (hash >> 2));
}
return hash;
}
};
//设置模板
template<size_t M, class K = std::string, class HashFunc1 = BKDRHash, class HashFunc2 = APHash, class HashFunc3 = DJBHash,
class HashFunc4 = JSHash>
//类实现
class Bloom_Filters
{
public:
void Set(const K& key)
{
size_t hashi1 = HashFunc1()(key) % M;//计算映射位置1
size_t hashi2 = HashFunc2()(key) % M;//计算映射位置2
size_t hashi3 = HashFunc3()(key) % M;//计算映射位置3
size_t hashi4 = HashFunc4()(key) % M;//计算映射位置4
//把位图的相关位置设置成1
_bs.set(hashi1);
_bs.set(hashi2);
_bs.set(hashi3);
_bs.set(hashi4);
}
bool Test(const K& key)
{
//4个位置的值必须同时为1才返回True
size_t hash1 = HashFunc1()(key) % M;
if (_bs.test(hash1) == false)
{
return false;
}
size_t hash2 = HashFunc2()(key) % M;
if (_bs.test(hash2) == false)
{
return false;
}
size_t hash3 = HashFunc3()(key) % M;
if (_bs.test(hash3) == false)
{
return false;
}
size_t hash4 = HashFunc4()(key) % M;
if (_bs.test(hash4) == false)
{
return false;
}
return true; // 即使到了这一步也存在误判
}
private:
std::bitset<M>_bs;
};
void Test1()
{
// 插入10个值
//BloomFilter<43, string, BKDRHash, APHash, DJBHash> bf;
Bloom_Filters<43> bf;
std::string a[] = { "苹果", "香蕉", "西瓜", "111111111", "eeeeeffff", "草莓", "休息", "继续", "查找", "set" };
for (auto& e : a)
{
bf.Set(e);
}
for (auto& e : a)
{
std::cout << bf.Test(e) << std::endl;
}
std::cout << std::endl;
std::cout << bf.Test("芒果") << std::endl;
std::cout << bf.Test("string") << std::endl;
std::cout << bf.Test("ffffeeeee") << std::endl;
std::cout << bf.Test("31341231") << std::endl;
std::cout << bf.Test("ddddd") << std::endl;
std::cout << bf.Test("3333343") << std::endl;
}
}
-
运行结果
-
误判率测试代码
void TestBloomFilter2()
{
srand(time(0));
const size_t N = 100000;
Bloom_Filters<8 * N> bf;
std::vector<std::string> v1;
std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
for (size_t i = 0; i < N; ++i)
{
v1.push_back(url + std::to_string(1234 + i));
}
for (auto& str : v1)
{
bf.Set(str);
}
/*for (auto& str : v1)
{
cout << bf.Test(str) << endl;
}
cout << endl << endl;*/
std::vector<std::string> v2;
for (size_t i = 0; i < N; ++i)
{
std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
url += std::to_string(999999 + i);
v2.push_back(url);
}
size_t n2 = 0;
for (auto& str : v2)
{
if (bf.Test(str))
{
++n2;
}
}
std::cout << "相似字符串误判率:" << (double)n2 / (double)N << std::endl;
std::vector<std::string> v3;
for (size_t i = 0; i < N; ++i)
{
std::string url = "zhihu.com";
url += std::to_string(rand());
v3.push_back(url);
}
size_t n3 = 0;
for (auto& str : v3)
{
if (bf.Test(str))
{
++n3;
}
}
std::cout << "不相似字符串误判率:" << (double)n3 / (double)N << std::endl;
}
- 运行结果: