👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨
一、问题引入
随着玩王者荣耀的用户越来越多,想要取一个心悦的名字是非常难的。当名字重复的时候,系统非常快的提示“名字已被使用”,那么它是如何做到快速查找名字是否被使用了呢?
在如此大的数据下,首先可以想到使用位图:
-
但由于位图的数据必须是
size_t
的,那么需要先将名字(字符串)映射成整型,然后再存入位图中。然而,我们对位图开空间是根据数据范围的,但字符串的长度是可变的,使用位图来表示长度不固定的数据会带来很大的挑战。比如,开少了可能引发冲突,那么就会导致误判。比如说李白和百里都占同一个比特位并且都标记成1
,当有人将修改了百里这一名称,那么其实就是reset
操作,那么,李白也同样会被reset
,那么就会有第二个人可以取李白这个名字。 -
并且字符串在计算机中是以字符集编码的方式存储的,一个字符可能由一个或多个字节组成。而位图一般用于表示集合中元素的存在与否,对于字符集编码来说,字符的种类和编码范围非常广泛,使用位图来表示所有可能的字符将需要非常大的存储空间,并且不实际。
这时候就需要我们本文中的主角: 布隆过滤器Bloomfilter
二、布隆过滤器的概念
布隆过滤器是由布隆在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询。可以用来告诉你 “某样东西一定不存在或者可能存在”
布隆过滤器是如何做到的呢?
这里举一个小故事:假设布隆有一天要去线下见网友,前提是他不知道网友的任何特征,那怎么办?只能发一个消息问对方今天的穿搭,而网友说今天带了一个黑色的帽子,然而布隆一眼望去,今天穿戴黑色帽子的人也特别的多,于是再次向网友询问更多的特征来过滤掉更多穿搭相识的人。
于是布隆过滤器就是 使用多个哈希函数,将一个数据映射到位图结构中,也就是 一个数据映射位图的多个位置(哈希与位图结合,即布隆过滤器)。只有当映射的比特位全为1
,则说明字符串存在。
三、布隆过滤器的实现
3.1 基本结构
既然需要多个哈希函数,这里我只模板中添加三个哈希函数的模板参数,以及待存储的数据类型,不过布隆过滤器最常见存储的类型是字符串,所以可以给一个缺省值。
显然,这三个 哈希函数 的选择是十分重要的,我们在这里提供三种较为优秀的哈希函数(字符串哈希算法),分别是 BKDRHash
、APHash
以及 DJBHash
struct BKDRHash
{
size_t operator()(const string& str)
{
register size_t hash = 0;
for (auto& ch : str)
{
hash = hash * 131 + (size_t)ch;
}
return hash;
}
};
struct APHash
{
size_t operator()(const string& str)
{
size_t hash = 0;
for (auto e : str)
{
if (((size_t)e & 1) == 0)
{
hash ^= ((hash << 7) ^ (size_t)e ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ (size_t)e ^ (hash >> 5)));
}
}
return hash;
}
};
struct DJBHash
{
size_t operator()(const string& str)
{
if (str.empty())
return 0;
size_t hash = 5381;
for (auto e : str)
{
hash += (hash << 5) + (size_t)e;
}
return hash;
}
};
template<size_t N
, class K = string
, class Hashfunc1 = BKDRHash
, class Hashfunc2 = APHash
, class Hashfunc3 = DJBHash>
class bloomfilter
{
public:
private:
bitset<N> _bt;
};
3.2 插入
步骤:
-
通过三个哈希函数计算不同的哈希值
-
最后再调用
bitset
中的set
在位图中标记即可
3.3 查找
判断在不在也非常简单,我们可以再次计算出三个哈希值,只要它们对应的二进制位上是1
,说明要查找的字符串存在,只要有一个为0
,说明要查找的字符串一定不存在。
这里需要注意的是:布隆过滤器判断不在
是准确的,判断 在
是不准确的
比如,“李白“映射了位置0 1 3
,”百里“映射了位置3 5 7
,虽然这两个字符串不会相互影响,但原本”妲己“不存在位图中,并且通过调用test
发现三个位置都是1
,那么就会误判存在。
因此 布隆过滤器并不是非常可靠,那该怎么办呢?有人想到了使用 布隆过滤器 + 数据库 的方式进行双重验证
如果 布隆过滤器 判断字符串不存在,那么就是真的不存在,因为这是绝对准确的;但如果在布隆过滤器中存在,可以去数据库里搜索,再反馈给用户。
3.4 删除
布隆过滤器支持删除吗?其实一般是不支持的。因为支持删除会存在重大问题,删除一个值会影响其他字符串。
虽然成功删除了 “李白”,但同时也影响了 “百里”,当要验证“百里”是否存在时,会被判断为不存在,所以布隆过滤器,一般是不支持删除操作的。
3.5 测试
3.6 优化
如果真的误判存在了那该怎么办呢?有的人想增加哈希函数的个数,那么问题来了,增加多少个合适呢?
还有一个比较合适的方案:扩大布隆过滤器的长度,使数据更分散,来降低误判的概率。
那么如何选择合适的布隆过滤器的长度呢?对此有大佬做出了研究:
我们可以来估算一下,假设用3
个哈希函数,即K = 3
,ln2
的值我们取 0.7
,那么 m
和 n
的关系大概是 m = n×k/ln2=4.2n
,也就是过滤器长度应该是插入元素个数的 4 -5倍
四、总结
优点:
-
增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
-
哈希函数相互之间没有关系,方便硬件并行运算
-
布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
-
在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
-
数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
-
使用同一组散列函数的布隆过滤器可以进行交、并、差运算
缺点:
-
有误判率,不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
-
不能获取元素本身
-
一般情况下不能从布隆过滤器中删除元素
实际应用场景:
-
注册时对于 昵称、用户名、手机号的验证
-
减少磁盘
IO
或者网络请求,因为一旦一个值必定不存在的话,我们可以不用进行后续昂贵的查询请求
五、哈希分割
5.1 面试题1
给两个文件,分别有
100
亿个query
语句,我们只有1G
内存,如何快速找到两个文件的交集?分别给出精确算法和近似算法。
- 近似解法:数据量大且
query
是由字符串组成的,因此可以借助布隆过滤器。先存储其中一个文件的query
语句,再从另一个文件读取query
语句,判断是否在布隆过滤器中,如果存在就是交集。并且由于布隆过滤器判断在不在会存在误判,符合题目要求的近似算法。 - 精确算法:假设平均一个
query
语句占30byte
,那么一个文件则占3000亿byte
,换算一下(1G ≈ 10亿byte+
),单个文件占300G
内存空间,两个文件一个600G
。然后将单个文件分割成1000
个小文件,每个小文件的大小为300Mb
;另一个文件也是如此,最后再读取到内存一一配对找交集即可。
但是这样一一配对也太慢了。因此这里介绍一个新玩法:哈希分割。哈希值i = HashFunc(query) % 1000
这意味着:不同的query
会得到不同的下标i
,而相同的query
会进入下标相同的小文件中,经过哈希切割后,只需要将对应A[i]
和B[i]
配对进行求交集就行了,这样能大大提高效率。
但是现在还存在一个问题:哈希值相同怎么办?那么这会导致单个小文件很大。
解决方法很简单:借助不同的哈希函数再分割。
对于大于300Mb
的小文件,我们可以对其进行读取
-
首先准备一个
unorder_set
,目的很简单:去重 -
读取文件中的
query
语句,存入unordered_set
中 -
如果小文件读取结束后,没有发生异常情况,说明属于情况1:大多都是相同、重复的
query
语句,把这些重复率高的数据打散,放置其他512 Mb
的小文件中 -
如果小文件读取过程中,出现了一个异常,捕获结果为
bad_alloc
,说明读取到的大多都是不重复的query
语句,因为我们内存只有1 GB
,抛出的异常是内存爆了
,异常的抛出意味着这个小文件属于情况2,可以使用其他的 哈希函数 对其进行再分割,分成512 Mb
的小文件
如此一来,这个文件就被解决了,核心在于:利用哈希切割将数据分为有特性的小文件、利用抛异常得知小文件的实际情况
5.2 面试题2
给一个超过
100G
大小的log
文件,log
中存着IP
地址, 设计算法找到出现次数最多的IP
地址?如何找到top K
的IP
?如何直接用Linux
系统命令实现?
同样使用哈希分割,将log
文件分割成大小相同的小文件
这里分为小文件的目的是:让相同的IP
分至同一个小文件中。
读取单个小文件时,利用unordered_map
统计IP
地址的出现次数,读取完毕后,遍历unordered_map
即可得知出现次数最多的IP
地址。
涉及Top K
的问题都可以通过 优先级队列pirority_queue
解决,在第一问的基础上,构建一个大小为 K
的小堆,筛选出Top K
个 IP
即可
至于如何利用 Linux
命令解决?
sort log_file | uniq -c | sort -nrk1,1 | head -K
解释:
sort log_file
表示对log_file
文件进行排序uniq -c
表示统计出其中每个IP
的出现次数sort -nrk1,1
表示按照每个IP
的出现次数再进行排序head -k
表示选择前k
个IP
地址显示
六、代码
本篇博客相关代码:点击跳转