C+学习——浅谈位图与布隆过滤器

1.位图

所谓位图,就是用每一位来存放某种状态,适用于大规模数据,但数据状态又不是很多的情况。通常是用来判断某个数据存不存在的。

题目:
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快 速判断一个数是否在这40亿个数中。 【腾讯】

思路:
这道题首先要判断40亿个不重复的无符号整数究竟占多大的内存,因为太大的内存我们无法加载到现有的计算机中。

一个整数是4个字节,40亿个整数就是160亿个字节,也就相当于16G内存,就一般的计算机而言很难实现这个加载,所以我们可以采取以下两种方案,一种是分割,一种是位图。

方法:
①分割
采用分割处理,把40亿个数分批次处理完毕,当然可以实现我们最终的目标,但是这样做时间复杂度未免优点太高。

②位图BitMap
在介绍这种方法前我首先来介绍一下什么是位图。

位图BitMap:位图是一个数组的每一个数据的每一个二进制位表示一个数据,0表示数据不存在,1表示数据存在。
在这里插入图片描述
如上所示,当我们需要存放一个数据的时候,我们需要安装以下方法:

1.首先确定这个数字在整个数据的哪一个数据(区间)。
2.确定这个数据(区间)的哪一个Bit位上。
3.在这个位上置1即可。

位图的实现代码

#include <iostream>
#include <vector>
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;
};

2.布隆过滤器

1.布隆过滤器的提出
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?

  1. 用哈希表存储用户记录,缺点:浪费空间
  2. 用位图存储用户记录,缺点:不能处理哈希冲突
  3. 将哈希与位图结合,即布隆过滤器

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

猜你喜欢

转载自blog.csdn.net/tonglin12138/article/details/93382025