所谓位图,就是用每一位来存放某种状态,适用于大规模数据,但数据状态又不是很多的情况。通常是用来判断某个数据存不存在的。
题目:
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快 速判断一个数是否在这40亿个数中。 【腾讯】
思路:
这道题首先要判断40亿个不重复的无符号整数究竟占多大的内存,因为太大的内存我们无法加载到现有的计算机中。
一个整数是4个字节,40亿个整数就是160亿个字节,也就相当于16G内存,就一般的计算机而言很难实现这个加载,所以我们可以采取以下两种方案,一种是分割,一种是位图。
方法:
①分割
采用分割处理,把40亿个数分批次处理完毕,当然可以实现我们最终的目标,但是这样做时间复杂度未免优点太高。
②位图BitMap
在介绍这种方法前我首先来介绍一下什么是位图。
位图BitMap:位图是一个数组的每一个数据的每一个二进制位表示一个数据,0表示数据不存在,1表示数据存在。
如上所示,当我们需要存放一个数据的时候,我们需要安装以下方法:
1.首先确定这个数字在整个数据的哪一个数据(区间)。
2.确定这个数据(区间)的哪一个Bit位上。
3.在这个位上置1即可。
位图的实现代码:
#include
#include
using namespace std;
class BitMap
{
public:
BitMap(size_t range)
{
//此时多开辟一个空间
_bits.resize(range / 32 + 1); //size为要处理的的数据总数,
//一个整型可以存放32个数据状态,所以这里vector的size为size/32
}
void Set(size_t x)
{
int index = x / 32;//确定哪个数据(区间)
int temp = x % 32;//确定哪个Bit位
_bits[index] |= (1 << temp);//位操作即可
}
void Reset(size_t x)
{
int index = x / 32;
int temp = x % 32;
_bits[index] &= ~(1 << temp);//取反
}
bool Test(size_t x)
{
int index = x / 32;
int temp = x % 32;
if (_bits[index]&(1<<temp))
return 1;
else
return 0;
}
// 位图中比特为1的个数
size_t Count()const
{
const char* pCount = "\0\1\1\2\1\2\2\3\1\2\2\3\2\3\3\4";
size_t size = _bit.size();
size_t count = 0;
for(size_t i = 0; i < size; ++i)
{
int value = _bit[i];
int j = 0;
while(j < sizeof(_bit[0]))
{
char c = value;
count += pCount[c&0x0f];
c >>= 4;
count += pCount[c&0x0f];
++j;
value >>= 8;
}
}
return count;
}
private:
vector<int> _bits;
};
1.布隆过滤器的提出
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
2.什么是布隆过滤器
本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。
3.布隆过滤器数据结构
布隆过滤器是一个 bit 向量或者说 bit 数组,长这样:
如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,则上图转变为:
Ok,我们现在再存一个值 “tencent”,如果哈希函数返回 3、4、8 的话,图继续变为:
值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “dianping” 这个值是否存在,哈希函数返回了 1、5、8三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “dianping” 这个值不存在。而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu” 存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。
这是为什么呢?答案跟简单,因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 “taobao” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 “taobao” 这个值存在。
4.下面就结合位图和哈希思想,我们实现布隆过滤器:
// 假设布隆过滤器中元素类型为K,每个元素对应5个哈希函数
template<class K, class KToInt1 = KeyToInt1,
class KToInt2 = KeyToInt2,
class KToInt3 = KeyToInt3,
class KToInt4 = KeyToInt4,
class KToInt5 = KeyToInt5>
class BloomFilter
{
public:
BloomFilter(size_t size) // 布隆过滤器中元素个数
: _bmp(10*size)
, _size(0)
{}
private:
BitMap _bmp;
size_t _size; // 实际元素的个数
}
bool Insert(const K& key)
{
size_t bitCount = _bmp.Size();
size_t index1 = KToInt1()(key)%bitCount;
size_t index2 = KToInt2()(key)%bitCount;
size_t index3 = KToInt3()(key)%bitCount;
size_t index4 = KToInt4()(key)%bitCount;
size_t index5 = KToInt5()(key)%bitCount;
_bmp.Set(index1);
_bmp.Set(index2);
_bmp.Set(index3);
_bmp.Set(index4);
_bmp.Set(index5);
_size++;
}
布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。
注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。
bool IsInBloomFilter(const K& key)
{
size_t bitCount = _bmp.Size();
size_t index1 = KToInt1()(key)%bitCount;
if(!_bmp.Test(index1))
return false;
size_t index2 = KToInt2()(key)%bitCount;
if(!_bmp.Test(index2))
return false;
size_t index3 = KToInt3()(key)%bitCount;
if(!_bmp.Test(index3))
return false;
size_t index4 = KToInt4()(key)%bitCount;
if(!_bmp.Test(index4))
return false;
size_t index5 = KToInt5()(key)%bitCount;
if(!_bmp.Test(index5))
return false;
return true; // 有可能在
}
目前我们知道布隆过滤器可以支持 add 和 isExist 操作,那么 delete 操作可以么,答案是不可以,例如上图中的 bit 位 4 被两个值共同覆盖的话,一旦你删除其中一个值例如 “tencent” 而将其置位 0,那么下次判断另一个值例如 “baidu” 是否存在的话,会直接返回 false,而实际上你并没有删除它。
如何解决这个问题,答案是计数删除。但是计数删除需要存储一个数值,而不是原先的 bit 位,会增大占用的内存大小。这样的话,增加一个值就是将对应索引槽上存储的值加一,删除则是减一,判断是否存在则是看值是否大于0。
很显然,过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。
另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高。
常见的适用常见有,利用布隆过滤器减少磁盘 IO 或者网络请求,因为一旦一个值必定不存在的话,我们可以不用进行后续昂贵的查询请求。
另外,既然你使用布隆过滤器来加速查找和判断是否存在,那么性能很低的哈希函数不是个好选择,推荐 MurmurHash、Fnv 这些。
这里推荐一篇文章写得不错,大家可以作为参考:https://www.jianshu.com/p/2104d11ee0a2