目录
一. 位图
1.1 概念
所谓位图(bitset),就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
例如:给你40亿个不重复的无符号整数,没有排过序,给你一个无符号整数,如何快速判断这个树在不在这40亿个数之中?
第一眼看是不是人都傻了,40亿个整数,相当于16G的内存,如果我们将他放到一个数组中,这个数组的长度就爆炸了,而且内存也开不了这么大的空间。那怎么办呢?
我们放在数组中是以一个整型的形式存储的,那我们是不是可以缩小一下用一个bite位来看呢,是不是这个内存占用就减小了8倍,是不是大大节约了空间。
这就是我们位图的基本思想,那好我们就用每一个bite位的状态(0或者1)来显示在不在。
1.2 bitset使用
根据上面的分析,可以看出我们位图有着三个基本的实现。
- 将一个bite位的状态变为1。
- 将一个bite位的状态变为0。
- 检查这个数所在的位置状态是不是1,为1就返回true。
set(size_t pos);//将pos数所映射的bit位标记为1
reset(size_t pos);//将pos数所映射的bit标记回0
test(size_t pos);//检测pos数所映射的bit为是否为1,为1则返回true
1.3 实现
我们这里用vector<int>来实现位图这个结构,为什么要用int呢?因为我们是用32位bite位为一个单位,刚好是一个整形,所以就用int,也可以用char。
那么如果有N个数,首先对vector进行开空间,我们这里要开N/32+1个整型空间,并把每个位置的初始值给0,为什么要多开一个呢?因为不能保证N一定是32的倍数,可能会多余几个数,所以多开32个bite位,即+1。而且即使浪费也只多浪费一个整型。
- set函数:将某个数字对应的位置的状态变为1。具体操作是:首先看这个数是位于第几个空间(一个空间32个bite位)-》x/32,再看这个数在这个空间的那个位置-》x%32,那么让这个位置的状态变为1,可以用到或(|)运算。
- reset函数:将某个数字对应的位置的状态变为0。具体操作是:还是先找到位于哪个空间的哪个位置,然后用与(&)运算。
- test函数:检测某个数的状态是否为1。具体操作是:找到这个数的位置,然后用这个位置的状态与运算1。
//把x映射的位置标记成1
void set(size_t x)
{
assert(x <= N);
size_t i = x / 32;
size_t j = x % 32;
_bits[i] |= (1 << j);//此处左移是向高位移的意思,不是普通意义上想左移
//此处是以一个整型为单位
}
//把x映射的位置标记成0
void reset(size_t x)
{
assert(x <= N);
size_t i = x / 32;
size_t j = x % 32;
_bits[i] &= ~(1 << j);
}
bool test(size_t x)
{
assert(x <= N);
size_t i = x / 32;
size_t j = x % 32;
return _bits[i] & (1 << j);
}
需要注意的是:我们这里不论是&还是|,都是对于同一bite位上的数来运算。因此要用到移位操作符(<<),这里左移指的是向高位移动,并不是书面上的向左移。
对于set来说:
对于reset来说:
对于test来说:
1.4 位图的应用
- 快速查找某个数据是否在一个集合中
- 排序 + 去重
- 求两个集合的交集、并集等
- 操作系统中磁盘块标记
二. 布隆过滤器
2.1 布隆过滤器的提出
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉 那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用 户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那 些已经存在的记录。 如何快速查找呢?
1. 用哈希表存储用户记录,缺点:浪费空间
2. 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理 了。
3. 将哈希与位图结合,即布隆过滤器
2.2 概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概 率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存 在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也 可以节省大量的内存空间。
布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位就为1。
2.3 布隆过滤器基本结构定义
布隆过滤器用到了位图的三个函数。
struct HashFuncBKDR
{
// BKDR
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto ch : s)
{
hash *= 131;
hash += ch;
}
return hash;
}
};
struct HashFuncAP
{
// AP
size_t operator()(const string& s)
{
size_t hash = 0;
for (size_t 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;
}
};
struct HashFuncDJB
{
// DJB
size_t operator()(const string& s)
{
size_t hash = 5381;
for (auto ch : s)
{
hash = hash * 33 ^ ch;
}
return hash;
}
};
template<size_t N,class K=string,
class Hash1=HashFuncBKDR,
class Hash2=HashFuncAP,
class Hash3=HashFuncDJB>
class BloomFilter
{
private:
static const size_t M = 5 * N;
std::bitset<M>* _bs=new std::bitset<M>;
};
2.4 插入
void set(const K& key)
{
size_t hash1 = Hash1()(key) % M;//匿名对象调用
size_t hash2 = Hash2()(key) % M;
size_t hash3 = Hash3()(key) % M;
_bs->set(hash1);
_bs->set(hash2);
_bs->set(hash3);
}
用哈希函数算出对应映射的位置,将位置的状态变为1即可。
2.5 查找
bool Test(const K& key)
{
size_t hash1= Hash1()(key) % M;
if (_bs->test(hash1) == false)
{
return false;
}
size_t hash2 = Hash2()(key) % M;
if (_bs->test(hash2) == false)
{
return false;
}
size_t hash3 = Hash3()(key) % M;
if (_bs->test(hash3) == false)
{
return false;
}
return true;
}
分别计算每个哈希值对应的比特位置存储的是否为0,只要有一个为0,代表该元素一定不在哈希表中,否则可能在哈希表中。
注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可 能存在,因为有些哈希函数存在一定的误判。
2.6 删除
布隆过滤器是不能支持删除的,因为不同的元素可能映射相同的位置,删除了某个元素后,可能改变了其他元素在对应位置的状态。
但是有一种删除方法是:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k分哈希函数计算处的哈希地址)+1,在删除元素时,给k个计数器-1,这样通过多占几倍的存储空间代价来增加删除操作。
2.7 布隆过滤器优点
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无 关
- 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
2.8 布隆过滤器缺点
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再 建立一个白名单,存储可能会误判的数据)
- 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素
- 如果采用计数方式删除,可能会存在计数回绕问题
三. 哈希切割
有这么一道题:给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同,如何找到top K的IP?
此处的话,可以用map<string,int>,属于kv模型,不能用布隆过滤器(布隆过滤器本质就是位图,是解决类型不是整型的时候的情况),我们可以用到哈希切割的思想。
我们可以依次取ip,让每个ip%100,这样就将ip分到100个不同的区间里面,那么相同的ip就在相同的区间了。
还有一个问题,可能出现某个ip太多,这样冲突很多,那么map就会抛异常,这时就需要更换哈希函数,进行二次切分处理。
而找topk的话,就可以建堆来处理。
四. 面试题
(1)给定100亿个整数,设计算法找到只出现一次的整数?
这个题还是用位图来解决。需要注意的是一种情况:如果我们位图需要1个G的内存,但是只有512MB怎么办呢?
我们可以分成两个位图来处理:第一个位图处理0~2^31-1的数据直接映射,第二个位图处理2^31~2^32-1的数据减去2^31之后再映射。
(2)给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
我们可以分别映射到两个位图中去,那么在两个位图相同位置状态都为1的元素就是交集。
(3)1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数。
我们还是可以用两个位图来解决,那么两个位图某个相同位置的状态组合起来就只有四种(00,01,10,11),我们用00代表0次,01代表1次,10代表两次,11代表3次及以上。那么只需看哪些是00,01,10的就可以看出了。
(4)给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出 精确算法和近似算法。
这里的近似算法就是我们的布隆过滤器,我们主要来讲讲精确算法。
我们可以对两个文件的query进行哈希切割(query%1000),分别是两个文件中的query进入两组1000个的文件组中,第一个文件的放到A中,第二个文件放到B中。A和B的query继续分别放到连个该set中,找交集即可。
(5)如何扩展BloomFilter使得它支持删除元素的操作
每个位置给多个bite的引用计数做标记,比如一个位置给8bite位做标记,但是这样空间消耗就高了。
总结
好了,到这里今天的知识就讲完了,大家有错误一点要在评论指出,我怕我一人搁这瞎bb,没人告诉我错误就寄了。
祝大家越来越好,不用关注我(疯狂暗示)