前言:
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
如何我们使用哈希表的话,如果太多的新闻,所需要的内存也是个巨大的问题
但是我们要使用位图的话,哈希冲突就是一个不可避免的问题!
由于推送新闻,我们允许有一定的错误发生!
我们就可以将哈希表和位图进行结合,也就是我们本文要说的布隆过滤器
布隆过滤器
它算是一种概率型的数据结构,可以判断一个东西一定不存在或者有可能存在 。优点是插入和查询的效率,特别得高。
我们提到它是位图和哈希的重组,那怎么样重组的呢?
在位图中我们仅限于整形,使用了直接定址法确定位置;如果我们不判断整形,那么就需要使用哈希函数映射为整形,放在位图。如果我们仅存于一个位置,发生哈希冲突就会造成“双兔傍地走,安能辨我是雄雌?”的局面。
我们需要避免这种情况,怎么避免呢?如果使用多个哈希函数进行映射,映射到不同位置,发生哈希冲突的概率真的就极低了。但是这也造就了布隆过滤器的不准确特点:判断存在时不准确。
废话这么多,布隆过滤器就是个什么东西!
它的原理是用多个哈希函数,将数据映射到位图中;
这样既可以提高效率,又节省了空间。但是存有一个特点(缺点):判断一个东西只能说有可能存在,不能准确判断。
我们刚才提到了N个布隆过滤器的缺点,那么原因是什么呢?
我们接下来阐述:
布隆过滤器的缺点产生原因:
布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,被映射的位置的比特位一定为1.
我们在查找的时候呢?
利用哈希函数计算位置,判断每个位置存储的值是否其为1,只要有一个0,我们就判断该元素不在哈希表中
如果对应位置全为1,就是在嘛,这个我们要三思了。看下面:
倘若我们采用三个哈希函数,将元素“shusheng”映射到位图上1,4,7号位置,将元素“Qyuan”映射到位图上3,5,7号位置。
如果我要查找是否得到了offer ,输入了shusheng查找存在,好的往下输
输入了Qyuan查找存在,欣喜若狂
在输入offer,判断?我勒个去!我得到offer了吗?
请嘴角上扬!
抱歉,没有!这只是一种巧合,offer映射的位置,由于被其他元素全都映射过,所以呢?判断它的时候,恰巧判断存在
布隆过滤器的操作
我们熟悉一个东西总是从增删查改开始!上面我们学习了增和查的思想!
改好像是完全没必要的,这个场景不需要!如果需要的话,我们可以和删一起思考一下,仅仅是增就可能发生误判现象!如果删和改,那么就不是不能准确判断一个元素存在的情况了,就连不存在都不能准确判断,布隆过滤器似乎没有价值了
这么半天,只是说了一个问题:布隆过滤器只支持增和查!
倘若支持删,那么每个位置只能有一个元素映射,代价将是巨大的
代码实现:
#pragma once
#include<string>
#include"bitset.h"
struct HashStr1
{
// BKDR
size_t operator()(const std::string& str)
{
size_t hash = 0;
for (size_t i = 0; i < str.size(); ++i)
{
hash *= 131;
hash += str[i];
}
return hash;
}
};
struct HashStr2
{
// RSHash
size_t operator()(const std::string& str)
{
size_t hash = 0;
size_t magic = 63689; // 魔数
for (size_t i = 0; i < str.size(); ++i)
{
hash *= magic;
hash += str[i];
magic *= 378551;
}
return hash;
}
};
struct HashStr3
{
// SDBMHash
size_t operator()(const std::string& str)
{
size_t hash = 0;
for (size_t i = 0; i < str.size(); ++i)
{
hash *= 65599;
hash += str[i];
}
return hash;
}
};
template<class K = std::string, class Hash1 = HashStr1, class Hash2 = HashStr2, class Hash3 = HashStr3>
class bloomfilter{
public:
bloomfilter(const size_t n)
:_bitset(5 * n)
, _num(5* n)
{}
void set(const K& data)
{
Hash2 _hash;
size_t index1 = Hash1()(data) % _num;
size_t index2 = _hash(data) % _num;
size_t index3 = Hash3()(data) % _num;
_bitset.set(index1);
_bitset.set(index2);
_bitset.set(index3);
}
bool test(const K& data)
{
Hash2 _hash;
size_t index1 = Hash1()(data) % _num;
size_t index2 = _hash(data) % _num;
size_t index3 = Hash3()(data) % _num;
return _bitset.test(index1)&&_bitset.test(index2)&&_bitset.test(index3);
}
private:
BitSet::bitset _bitset;
size_t _num; //有效数据
};
布隆过滤器的应用
给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和 近似算法
近似算法呢?我们使用我们的布隆过滤器,将一个文件所有的query全部映射到我们的布隆过滤器上,在遍历另一个文件,判断该query是否在布隆过滤器上存在;
由于布隆过滤器可能将某个不在的query误判成存在!所以这种只能算是近似算法 。
精确算法:因为100亿个query占用的内存过大,所以我们直接遍历两个文件对比!倘若我们将它们切割成我们能遍历的的大小,比如将一个文件切割成1000份乃至2000份,我们就可以将小文件的内容存入容器,查找交集。
但是问题又来了,我们怎么分割呢?
倘若随机分割,第一个文件分成1000份,每份文件为
A1 A2 A3 .............................................. A999
B1 B2 B3 .............................................. B999(第二个文件)
我们就需要用A1和每一个B开头的文件进行查找交集的行为,A2,A3.....A999同样如此。只有这样,我们才能够精确地查找交集。
但是分割的办法变一下呢?我们利用哈希函数进行分隔!
相同的query一定进行相同序号的文件
那么我们只需要A1和B1 查交集 A2和B2 查交集。。。。。。。
这样我们的效率更高一些。
给一个超过100G大小的log fifile, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
IP地址大部分用字符串表示,无论如何我们不可能直接遍历100G大小的文件。我们依旧利用哈希函数将大文件分隔成我们能遍历的小文件,每个小文件key-value模型的容器存储,我们就可以取得每个IP出现的次数,找到出现次数最多的哪一个也就不成什么问题了。
要找出现次数前几个的IP地址
就是平常的topK问题,直接上堆。
我们总结一下:
布隆过滤器只能判断元素在不在,并且有一定的误判,但是插入查找效率特别高,存储空间少