布隆过滤器
布隆过滤器提出:
针对推送去重问题提出。
当服务器记录用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
这里提供了三种方法:
- 用哈希表存储用户记录,缺点:浪费空间
- 用位图存储用户记录,缺点:不能处理哈希冲突
- 将哈希与位图结合,即布隆过滤器
布隆过滤器概念:
是一种紧凑型的、比较巧妙的概率型数据结构。
特点:
高效的插入和查询,因为存在一定的概率型,所以只能告诉用户“某样东西一定不存在或者可能存在”。
实质:
利用多个哈希函数,讲一个数据映射到位图结构中。
优点:
提升查询效率,节省大量的内存空间。
布隆过滤器的插入、查找和缺陷:
将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位为1.
查找方法:
分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。
插入baidu和tencent
baidu置1位为:1 4 7
tencent置1位为:3 4 8
若我们新插入alibaba
alibaba置1位为:1 3 8
此时插入的数据与之前已有的数据发生了重叠。
若此时要寻找baidu,只需找到1 4 7位置1的即可。
若此时要寻找toutiao,toutiao的置1位为:4 7 8
因为4 7 8此时均被置1,所以布隆过滤器会显示toutiao在这个布隆过滤器内。实际上并不存在。
这就是布隆过滤器的缺陷。
注意:
因为有些哈希函数存在一定的误判,所以会出现意外情况:即布隆过滤器如果说某个元素不存在时,该元素一定不存在。
如果说某个元素存在时,该元素可能存在。
布隆过滤器的删除:
以上图为例:
若要求删除tencent元素,如果直接将tencent的3 4 8位直接置为0,此时baidu元素与tencent元素在比特位上有重叠,所以baidu元素也将被删除。会造成误删。
支持的删除方法:
将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
缺陷:
1.无法确认元素是否真正在布隆过滤器中
2. 存在计数回绕
布隆过滤器的优点:
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
- 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
布隆过滤器的缺陷:
- 有误判率,即存在假阳性,即不能准确判断元素是否在集合中(补救方法:再建立一个白
名单,存储可能会误判的数据) - 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素
- 如果采用计数方式删除,可能会存在计数回绕问题
#pragma once
namespace bite
{
template<size_t N, class T,
class HF1,
class HF2,
class HF3,
class HF4,
class HF5>
class BloomFilter
{
public:
BloomFilter()
:_size(0)
{}
void Insert(const T& data)
{
size_t index1 = HF1()(data) % N;
size_t index2 = HF2()(data) % N;
size_t index3 = HF3()(data) % N;
size_t index4 = HF4()(data) % N;
size_t index5 = HF5()(data) % N;
_bs.set(index1);
_bs.set(index2);
_bs.set(index3);
_bs.set(index4);
_bs.set(index5);
++_size;
}
bool IsIn(const T& data)
{
size_t index = HF1()(data);
if (_bs.text(index))
return false;
size_t index = HF2()(data);
if (_bs.text(index))
return false;
size_t index = HF3()(data);
if (_bs.text(index))
return false;
size_t index = HF4()(data);
if (_bs.text(index))
return false;
size_t index = HF5()(data);
if (_bs.text(index))
return false;
}
private:
bitset<N> _bs;
};
}
题:
1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法。
100亿*4字节=40G
精确算法:哈希切分
对两个文件进行哈希切分,使用相同的散列函数(下附五个散列函数)将所有query转换为整数hash。再利用index(文件下标)=hash%1000可将相同的query分到同一个文件。
将两个文件中index相同的小文件进行对比,找出其交集。
将文件的交集汇总起来即为整个文件的交集。
算法复杂度为O(N)。
近似算法:布隆过滤器
首先使用相同的散列函数(下附五个散列函数)将所有query转换为整数hash。
将第一个文件中的整数映射到位图中,再拿第二个文件中的数字与第一个文件映射的位图去对比。存在相同数字即为交集。
算法复杂度为O(N)。
2. 如何扩展Bloom Filter使得它支持删除元素的操作
对每一个位进行引用计数。
散列函数:
template<class T>
size_t BKDRHash(const T* str)
{
register size_t hash = 0;
while (size_t ch = (size_t)*str++)
{
hash = magic * 131+ ch;
}
return hash;
}
template<class T>
size_t SDBMHash(const char* str)
{
register size_t hash = 0;
while (size_t ch = (size_t)*str++)
{
hash = 65599 * hash + ch;
}
return hash;
}
template<class T>
size_t RSHash(const T* str)
{
register size_t hash = 0;
size_t magic = 63689;
while (size_t ch = (size_t)*str++)
{
hash = magic * hash + ch;
magic *= 378551;
}
return hash;
}
template<class T>
size_t APHash(const T* str)
{
register size_t hash = 0;
size_t ch;
for (long i = 0; ch = (size_t)*str++; i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ ch ^ (hash > >3));
}
else
{
hash ^=(~ ((hash << 11) ^ ch ^ (hash >> 5)));
}
}
return hash;
}
template<class T>
size_t JSHash(const T* str)
{
if (!*str)
return 0;
register size_t hash = 1315423911;
while (size_t ch = (size_t)*str++)
{
hash ^= ((hash << 5) ^ ch ^ (hash >> 2));
}
return hash;
}
template<class T>
size_t DEKHash(const T* str)
{
if (!*str)
return 0;
register size_t hash = 1315423911;
while (size_t ch = (size_t)*str++)
{
hash ^= ((hash << 5) ^ ch ^ (hash >> 27))^ch;
}
return hash;
}
template<class T>
size_t FNVHash(const T* str)
{
if (!*str)
return 0;
register size_t hash = 1315423911;
while (size_t ch = (size_t)*str++)
{
hash *= 16777619;
hash ^= ch
}
return hash;
}
倒排索引
概念:
倒排索引不是由记录来确定属性值的,而是由属性值来确定记录的位置。
组成:
单词词典和倒排文件。
倒排索引一般表示为一个关键词,然后是它的频度(出现的次数),位置(出现在哪一篇文章或网页中,及有关的日期,作者等信息),它相当于为互联网上几千亿页网页做了一个索引,好比一本书的目录、标签一般。读者想看哪一个主题相关的章节,直接根据目录即可找到相关的页面。不必再从书的第一页到最后一页,一页一页的查找。
简单来说就是[文章——关键词]颠倒为[关键词——文章]。
题:
1.给上千个文件,每个文件大小为1K—100M。给n个词,设计算法对每个词找到所有包含它的文件,你只有100K内存。
答:对上千个文件生成1000个布隆过滤器,并将1000个布隆过滤器存入一个文件中,将内存分为两份,一份用来读取布隆过滤器中的词,一份用来读取文件, 直到每个布隆过滤器读完为止。
用一个文件info 准备用来保存n个词和包含其的文件信息。
首先把n个词分成x份。对每一份用生成一个布隆过滤器(因为对n个词只生成一个布隆过滤器,内存可能不够用)。把生成的所有布隆过滤器存入外存 的一个文件Filter中。
将内存分为两块缓冲区,一块用于每次读入一个 布隆过滤器,一个用于读文件(读文件这个缓冲区使用 相当于有界生产者消费者问题模型来实现同步),大文 件可以分为更小的文件,但需要存储大文件的标示信 息(如这个小文件是哪个大文件的)。
对读入的每一个单词用内存中的布隆过滤器来判 断是否包含这个值,如果不包含,从Filter文件中读 取下一个布隆过滤器到内存,直到包含或遍历完所有 布隆过滤器。如果包含,更新info 文件。直到处理完 所有数据。删除Filter文件。