目录
5.1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?
5.2. 给一个超过100G大小的 log file,log 中存着IP地址,设计算法找到出现次数最多的IP地址?如何找到top K的IP?
1. 布隆过滤器的概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉 "某样东西一定不存在或者可能存在",它是用多个哈希函数,将一个数据映射到位图结构中,此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
布隆过滤器的设计思路
我们发现,上面的映射关系可能不具备唯一性。
也就是说,当我们去判断某一个值存不存在的时候,可能具有误判,假设上面没有"野牛"这个字符串,但是当我们去判断"野牛"存不存在的时候,如果此时"大熊猫"是存在的,那么我们也会得到"野牛"是存在的结果,而如果此时"大熊猫"也不存在,那么我们会得到"野牛"是一定不存在的。
因此:
- 对于在的判断,是不准确的,存在误判;
- 但是对于不在的判断,是准确的,具有唯一性。
而对于这种误判,我们是不能做到完全杜绝的,但是我们却可以降低它的误判率。
那么如何降低误判率呢?我们可以让每个Key多映射几个位,如下图:
虽然此时,"野牛"和"大熊猫"有一个位置冲突了,但是它们剩余的位置却是不冲突的,即只有两个字符串所有映射的位置都冲突了,才会导致误判,因此,在一定程度上降低了误判率。
理论而言:
一个值映射的位越多,误判概率越低,但是也不敢映射太多,映射位太多,那么空间消耗就越大,优势就会被削弱。
而上面的方式,也就是布隆过滤器的大致实现思路。
2. 布隆过滤器的应用
布隆过滤器(Bloom Filter)是一种空间效率非常高的概率数据结构,用于判断一个元素是否属于一个集合。它可以用于快速过滤掉不属于集合中的元素,具有高效的查询速度和较小的内存占用。
布隆过滤器的应用场景包括但不限于以下几个方面:
- 1. 缓存:在缓存系统中,布隆过滤器可以用来判断一个待查询的数据是否存在于缓存中,从而避免对底层数据存储系统的查询操作,提高缓存的命中率。
- 2. 数据库查询优化:在数据库系统中,布隆过滤器可以用于减少对磁盘或网络的查询压力。例如,在查询之前,可以先使用布隆过滤器判断某个查询条件是否存在对应的记录,如果不存在,可以快速返回查询结果为空,避免不必要的查询操作。
- 3. 网络爬虫去重:在网络爬虫系统中,布隆过滤器可以用于去重操作。爬虫在爬取网页时,可以使用布隆过滤器来过滤已经抓取过的网页链接,避免重复抓取相同的内容。
- 4. 防止缓存穿透:在分布式系统中,布隆过滤器可以用于防止缓存穿透。当一个请求的查询结果不存在于缓存中时,可以先通过布隆过滤器进行快速判断,如果查询结果不存在于布隆过滤器中,可以直接拒绝该请求,避免对底层存储系统的过度查询负载。
需要注意的是,布隆过滤器在判断元素是否存在时,可能会存在一定的误判率(False Positive)。因此,在使用布隆过滤器时需要权衡误判率和内存占用,根据实际需求选择合适的参数配置和误判率控制策略。
由于黑名单的Key是整体的一小部分,而大多数的Key都不在黑名单,因此布隆过滤器可以快速过滤掉黑名单中不存在的Key,避免过多的操作,进而提高效率。
3. 布隆过滤器的实现
在开始实现之前,我们应该解决一个问题: 如何选择哈希函数个数和布隆过滤器长度,人们经过分析和实践,得出下面的结论:
在这里我们粗略计算,当 k = 3 时,插入1个元素,大概需要5个空间。
那么我们的实现如下:
class hash1_string //BKDRHash
{
public:
size_t operator()(const std::string& str)
{
size_t ret = 0;
for (auto ch : str)
{
ret *= 131;
ret += ch;
}
return ret;
}
};
class hash2_string //APHash
{
public:
size_t operator()(const std::string& str)
{
size_t hash = 0;
size_t ch = 0;
for (size_t i = 0;i < str.size(); i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
}
}
return hash;
}
};
class hash3_string //DJBHash
{
public:
size_t operator()(const std::string& str)
{
size_t hash = 5381;
for (auto ch : str)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
// 布隆过滤器大部分情况Key都是string
template<size_t N,class K = std::string,class Hash1 = hash1_string,class Hash2 = hash2_string,class Hash3 = hash3_string>
class bloom_filter
{
public:
void set(const K& key)
{
// 第一个哈希函数映射的位置
size_t hash_index1 = Hash1()(key) % (_ratio * 5);
_table.set(hash_index1);
// 第二个哈希函数映射的位置
size_t hash_index2 = Hash2()(key) % (_ratio * 5);
_table.set(hash_index2);
// 第三个哈希函数映射的位置
size_t hash_index3 = Hash3()(key) % (_ratio * 5);
_table.set(hash_index3);
}
bool test(const K& key)
{
size_t hash_index1 = Hash1()(key) % (_ratio * 5);
if (!_table.test(hash_index1)) // 不存在是确定的
return false;
size_t hash_index2 = Hash2()(key) % (_ratio * 5);
if (!_table.test(hash_index2)) // 不存在是确定的
return false;
size_t hash_index3 = Hash3()(key) % (_ratio * 5);
if (!_table.test(hash_index3)) // 不存在是确定的
return false;
return true; // 走到这里,说明可能存在,但不确定,可能误判
}
private:
const static size_t _ratio = 5; // 当哈希函数个数为3,插入一个数据,需要5个空间
bit_set<N*_ratio> _table;
};
布隆过滤器不能直接支持删除,因为某些Key可能会映射到同一个位置,删除会影响其他Key。
一种支持删除的方法:采用引用计数的思想 ,将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器 (k个哈希函数计算出的哈希地址) 加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
缺陷:
- 无法确认元素是否真正在布隆过滤器中,存在误判;
- 存在计数回绕。
4. 布隆过滤器的总结
布隆过滤器优点:
- 增加和查询元素的时间复杂度为:O(K),(K为哈希函数的个数,一般比较小),与数据量大小无关;
- 哈希函数相互之间没有关系,方便硬件并行运算;
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势;
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势;
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能;
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算。
布隆过滤器缺点:
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据);
- 不能获取元素本身;
- 一般情况下不能从布隆过滤器中删除元素;
- 如果采用计数方式删除,可能会存在计数回绕问题。
5. 布隆过滤器的拓展
5.1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?
在这里我们采用一种思想:哈希切分,大概思路如下:
注意:这里的分割并不是平均切分,而是根据 Key 取模后得到不同的值进入不同的文件。
5.2. 给一个超过100G大小的 log file,log 中存着IP地址,设计算法找到出现次数最多的IP地址?如何找到top K的IP?
关键点:虽然一个文件中可能有不同的ip,但是相同的ip一定在同一个文件。